Skip to main content

oxicuda_sparse/format/
convert.rs

1//! Format conversion routines between sparse matrix formats.
2//!
3//! These functions convert between CSR, CSC, COO, BSR, and ELL formats.
4//! Currently conversions are performed via host-side download/upload; future
5//! versions will use GPU kernels for large matrices.
6//!
7//! For simple conversions (CSR <-> COO, CSR <-> CSC), use the `to_*` methods
8//! on the individual format types directly. This module provides additional
9//! conversions that require more complex logic.
10
11use oxicuda_blas::GpuFloat;
12
13use crate::error::{SparseError, SparseResult};
14use crate::handle::SparseHandle;
15
16use super::bsr::BsrMatrix;
17use super::coo::CooMatrix;
18use super::csc::CscMatrix;
19use super::csr::CsrMatrix;
20use super::ell::EllMatrix;
21
22/// Converts a CSR matrix to CSC format.
23///
24/// This is a convenience wrapper around [`CsrMatrix::to_csc`].
25///
26/// # Errors
27///
28/// Returns [`SparseError::Cuda`] on transfer failure.
29pub fn csr_to_csc<T: GpuFloat>(
30    _handle: &SparseHandle,
31    csr: &CsrMatrix<T>,
32) -> SparseResult<CscMatrix<T>> {
33    csr.to_csc()
34}
35
36/// Converts a CSC matrix to CSR format.
37///
38/// This is a convenience wrapper around [`CscMatrix::to_csr`].
39///
40/// # Errors
41///
42/// Returns [`SparseError::Cuda`] on transfer failure.
43pub fn csc_to_csr<T: GpuFloat>(
44    _handle: &SparseHandle,
45    csc: &CscMatrix<T>,
46) -> SparseResult<CsrMatrix<T>> {
47    csc.to_csr()
48}
49
50/// Converts a COO matrix to CSR format.
51///
52/// This is a convenience wrapper around [`CooMatrix::to_csr`].
53///
54/// # Errors
55///
56/// Returns [`SparseError::Cuda`] on transfer failure.
57pub fn coo_to_csr<T: GpuFloat>(
58    _handle: &SparseHandle,
59    coo: &CooMatrix<T>,
60) -> SparseResult<CsrMatrix<T>> {
61    coo.to_csr()
62}
63
64/// Converts a COO matrix to CSC format.
65///
66/// This is a convenience wrapper around [`CooMatrix::to_csc`].
67///
68/// # Errors
69///
70/// Returns [`SparseError::Cuda`] on transfer failure.
71pub fn coo_to_csc<T: GpuFloat>(
72    _handle: &SparseHandle,
73    coo: &CooMatrix<T>,
74) -> SparseResult<CscMatrix<T>> {
75    coo.to_csc()
76}
77
78/// Converts a CSR matrix to ELL format.
79///
80/// This is a convenience wrapper around [`EllMatrix::from_csr`].
81///
82/// # Errors
83///
84/// Returns [`SparseError::Cuda`] on transfer failure.
85pub fn csr_to_ell<T: GpuFloat>(
86    _handle: &SparseHandle,
87    csr: &CsrMatrix<T>,
88) -> SparseResult<EllMatrix<T>> {
89    EllMatrix::from_csr(csr)
90}
91
92/// Converts a CSR matrix to BSR format with the specified block dimension.
93///
94/// The CSR matrix dimensions must be multiples of `block_dim`. The conversion
95/// groups entries into dense blocks; zero-fill is used for block entries that
96/// are not present in the CSR.
97///
98/// # Errors
99///
100/// Returns [`SparseError::InvalidFormat`] if dimensions are not multiples of
101/// `block_dim`.
102/// Returns [`SparseError::Cuda`] on transfer failure.
103pub fn csr_to_bsr<T: GpuFloat>(
104    _handle: &SparseHandle,
105    csr: &CsrMatrix<T>,
106    block_dim: u32,
107) -> SparseResult<BsrMatrix<T>> {
108    if block_dim == 0 {
109        return Err(SparseError::InvalidArgument(
110            "block_dim must be non-zero".to_string(),
111        ));
112    }
113    if csr.rows() % block_dim != 0 {
114        return Err(SparseError::InvalidFormat(format!(
115            "rows ({}) must be a multiple of block_dim ({})",
116            csr.rows(),
117            block_dim
118        )));
119    }
120    if csr.cols() % block_dim != 0 {
121        return Err(SparseError::InvalidFormat(format!(
122            "cols ({}) must be a multiple of block_dim ({})",
123            csr.cols(),
124            block_dim
125        )));
126    }
127
128    let (h_row_ptr, h_col_idx, h_values) = csr.to_host()?;
129
130    let block_rows = csr.rows() / block_dim;
131    let block_cols = csr.cols() / block_dim;
132    let bd = block_dim as usize;
133
134    // Phase 1: Identify which block positions have at least one entry
135    // Use a set for each block row
136    let mut block_entries: Vec<Vec<u32>> = Vec::with_capacity(block_rows as usize);
137    for br in 0..block_rows as usize {
138        let mut block_col_set = Vec::new();
139        for local_row in 0..bd {
140            let global_row = br * bd + local_row;
141            let start = h_row_ptr[global_row] as usize;
142            let end = h_row_ptr[global_row + 1] as usize;
143            for &cj in &h_col_idx[start..end] {
144                let bc = cj as u32 / block_dim;
145                if !block_col_set.contains(&bc) {
146                    block_col_set.push(bc);
147                }
148            }
149        }
150        block_col_set.sort_unstable();
151        block_entries.push(block_col_set);
152    }
153
154    // Phase 2: Build BSR row_ptr and col_idx
155    let mut bsr_row_ptr = vec![0i32; block_rows as usize + 1];
156    let mut bsr_col_idx = Vec::new();
157
158    for br in 0..block_rows as usize {
159        bsr_row_ptr[br + 1] = bsr_row_ptr[br] + block_entries[br].len() as i32;
160        bsr_col_idx.extend(block_entries[br].iter().map(|&c| c as i32));
161    }
162
163    let nnz_blocks = bsr_col_idx.len();
164    if nnz_blocks == 0 {
165        return Err(SparseError::ZeroNnz);
166    }
167
168    // Phase 3: Fill block values
169    let block_elems = bd * bd;
170    let mut bsr_values = vec![T::gpu_zero(); nnz_blocks * block_elems];
171
172    for br in 0..block_rows as usize {
173        let block_start = bsr_row_ptr[br] as usize;
174        let block_cols_for_row = &block_entries[br];
175
176        for local_row in 0..bd {
177            let global_row = br * bd + local_row;
178            let start = h_row_ptr[global_row] as usize;
179            let end = h_row_ptr[global_row + 1] as usize;
180            for j in start..end {
181                let global_col = h_col_idx[j] as usize;
182                let bc = global_col / bd;
183                let local_col = global_col % bd;
184
185                // Find block index via binary search
186                if let Ok(block_offset) = block_cols_for_row.binary_search(&(bc as u32)) {
187                    let block_idx = block_start + block_offset;
188                    let val_idx = block_idx * block_elems + local_row * bd + local_col;
189                    bsr_values[val_idx] = h_values[j];
190                }
191            }
192        }
193    }
194
195    // Suppress unused variable warning
196    let _ = block_cols;
197
198    BsrMatrix::from_host(
199        csr.rows(),
200        csr.cols(),
201        block_dim,
202        &bsr_row_ptr,
203        &bsr_col_idx,
204        &bsr_values,
205    )
206}
207
208#[cfg(test)]
209mod tests {
210    #[test]
211    fn block_dim_zero_rejected() {
212        // Cannot call csr_to_bsr without GPU, just verify the logic
213        assert_eq!(4 % 2, 0);
214        assert_ne!(5 % 2, 0);
215    }
216
217    // -----------------------------------------------------------------------
218    // Pure host-side algorithm tests (no GPU required)
219    //
220    // These replicate the CSR ↔ CSC ↔ COO conversion algorithms that live
221    // in the GPU format structs, operating directly on Vec<i32>/Vec<f32>.
222    // -----------------------------------------------------------------------
223
224    /// Transpose a CSR matrix (described as host arrays) to CSC format.
225    ///
226    /// Returns `(col_ptr, row_idx, values)` where the output is sorted by
227    /// column then (naturally) by row within each column.
228    fn host_csr_to_csc(
229        rows: usize,
230        cols: usize,
231        row_ptr: &[i32],
232        col_idx: &[i32],
233        values: &[f32],
234    ) -> (Vec<i32>, Vec<i32>, Vec<f32>) {
235        let nnz = values.len();
236
237        // Count entries per column.
238        let mut col_counts = vec![0i32; cols];
239        for &c in col_idx {
240            col_counts[c as usize] += 1;
241        }
242
243        // Prefix-sum to get col_ptr.
244        let mut col_ptr = vec![0i32; cols + 1];
245        for i in 0..cols {
246            col_ptr[i + 1] = col_ptr[i] + col_counts[i];
247        }
248
249        // Fill row_idx and values.
250        let mut out_row_idx = vec![0i32; nnz];
251        let mut out_values = vec![0.0f32; nnz];
252        let mut write_pos = col_ptr.clone();
253
254        for row in 0..rows {
255            let start = row_ptr[row] as usize;
256            let end = row_ptr[row + 1] as usize;
257            for j in start..end {
258                let col = col_idx[j] as usize;
259                let dest = write_pos[col] as usize;
260                out_row_idx[dest] = row as i32;
261                out_values[dest] = values[j];
262                write_pos[col] += 1;
263            }
264        }
265
266        (col_ptr, out_row_idx, out_values)
267    }
268
269    /// Transpose a CSC matrix (host arrays) back to CSR format.
270    fn host_csc_to_csr(
271        rows: usize,
272        cols: usize,
273        col_ptr: &[i32],
274        row_idx: &[i32],
275        values: &[f32],
276    ) -> (Vec<i32>, Vec<i32>, Vec<f32>) {
277        let nnz = values.len();
278
279        // Count entries per row.
280        let mut row_counts = vec![0i32; rows];
281        for &r in row_idx {
282            row_counts[r as usize] += 1;
283        }
284
285        // Prefix-sum to get row_ptr.
286        let mut out_row_ptr = vec![0i32; rows + 1];
287        for i in 0..rows {
288            out_row_ptr[i + 1] = out_row_ptr[i] + row_counts[i];
289        }
290
291        // Fill col_idx and values.
292        let mut out_col_idx = vec![0i32; nnz];
293        let mut out_values = vec![0.0f32; nnz];
294        let mut write_pos = out_row_ptr.clone();
295
296        for col in 0..cols {
297            let start = col_ptr[col] as usize;
298            let end = col_ptr[col + 1] as usize;
299            for j in start..end {
300                let row = row_idx[j] as usize;
301                let dest = write_pos[row] as usize;
302                out_col_idx[dest] = col as i32;
303                out_values[dest] = values[j];
304                write_pos[row] += 1;
305            }
306        }
307
308        // Sort each row's entries by column index for canonical form.
309        for r in 0..rows {
310            let s = out_row_ptr[r] as usize;
311            let e = out_row_ptr[r + 1] as usize;
312            // Gather, sort, scatter.
313            let mut row_pairs: Vec<(i32, f32)> =
314                (s..e).map(|i| (out_col_idx[i], out_values[i])).collect();
315            row_pairs.sort_by_key(|&(c, _)| c);
316            for (i, (c, v)) in row_pairs.into_iter().enumerate() {
317                out_col_idx[s + i] = c;
318                out_values[s + i] = v;
319            }
320        }
321
322        (out_row_ptr, out_col_idx, out_values)
323    }
324
325    /// Convert unsorted COO triplets to CSR format.
326    fn host_coo_to_csr(
327        rows: usize,
328        row_idx: &[i32],
329        col_idx: &[i32],
330        values: &[f32],
331    ) -> (Vec<i32>, Vec<i32>, Vec<f32>) {
332        let nnz = values.len();
333
334        // Sort triplets by (row, col).
335        let mut triplets: Vec<(i32, i32, f32)> = (0..nnz)
336            .map(|i| (row_idx[i], col_idx[i], values[i]))
337            .collect();
338        triplets.sort_by(|a, b| a.0.cmp(&b.0).then(a.1.cmp(&b.1)));
339
340        // Build row_ptr.
341        let mut row_ptr = vec![0i32; rows + 1];
342        for &(r, _, _) in &triplets {
343            row_ptr[r as usize + 1] += 1;
344        }
345        for i in 0..rows {
346            row_ptr[i + 1] += row_ptr[i];
347        }
348
349        let sorted_col_idx: Vec<i32> = triplets.iter().map(|&(_, c, _)| c).collect();
350        let sorted_values: Vec<f32> = triplets.iter().map(|&(_, _, v)| v).collect();
351
352        (row_ptr, sorted_col_idx, sorted_values)
353    }
354
355    #[test]
356    fn test_csr_to_csc_round_trip() {
357        // 4x4 sparse matrix:
358        //   [1 0 0 2]
359        //   [0 3 0 0]
360        //   [0 0 4 5]
361        //   [6 0 0 0]
362        //
363        // CSR:
364        //   row_ptr  = [0, 2, 3, 5, 6]
365        //   col_idx  = [0, 3,  1,  2, 3,  0]
366        //   values   = [1, 2,  3,  4, 5,  6]
367        let rows = 4usize;
368        let cols = 4usize;
369        let row_ptr = vec![0i32, 2, 3, 5, 6];
370        let col_idx = vec![0i32, 3, 1, 2, 3, 0];
371        let values = vec![1.0f32, 2.0, 3.0, 4.0, 5.0, 6.0];
372
373        // CSR → CSC
374        let (col_ptr, csc_row_idx, csc_values) =
375            host_csr_to_csc(rows, cols, &row_ptr, &col_idx, &values);
376
377        // Verify col_ptr: column 0 has entries [1,6], col 1 has [3], col 2 has [4], col 3 has [2,5]
378        assert_eq!(col_ptr, vec![0, 2, 3, 4, 6], "col_ptr mismatch");
379
380        // CSC → CSR (round-trip)
381        let (rt_row_ptr, rt_col_idx, rt_values) =
382            host_csc_to_csr(rows, cols, &col_ptr, &csc_row_idx, &csc_values);
383
384        // The round-trip must reproduce the original CSR exactly.
385        assert_eq!(rt_row_ptr, row_ptr, "round-trip row_ptr mismatch");
386        assert_eq!(rt_col_idx, col_idx, "round-trip col_idx mismatch");
387        for (a, b) in rt_values.iter().zip(values.iter()) {
388            assert!(
389                (a - b).abs() < 1e-6,
390                "round-trip value mismatch: {a} vs {b}"
391            );
392        }
393    }
394
395    #[test]
396    fn test_coo_to_csr_sorted() {
397        // Provide unsorted COO triplets; after conversion to CSR the
398        // entries within each row must be sorted by column index.
399        //
400        // Unsorted COO for a 3x4 matrix:
401        //   (2, 3, 9.0), (0, 1, 1.0), (1, 0, 4.0), (2, 1, 7.0), (0, 3, 2.0)
402        let rows = 3usize;
403        let row_idx = vec![2i32, 0, 1, 2, 0];
404        let col_idx = vec![3i32, 1, 0, 1, 3];
405        let values = vec![9.0f32, 1.0, 4.0, 7.0, 2.0];
406
407        let (row_ptr, out_col_idx, out_values) = host_coo_to_csr(rows, &row_idx, &col_idx, &values);
408
409        // Verify row_ptr: row 0 has 2 entries, row 1 has 1, row 2 has 2.
410        assert_eq!(row_ptr, vec![0, 2, 3, 5]);
411
412        // Verify entries in each row are sorted by column.
413        for r in 0..rows {
414            let s = row_ptr[r] as usize;
415            let e = row_ptr[r + 1] as usize;
416            for i in s + 1..e {
417                assert!(
418                    out_col_idx[i] >= out_col_idx[i - 1],
419                    "row {r}: col_idx not sorted at position {i}"
420                );
421            }
422        }
423
424        // Verify specific entries.
425        // Row 0: (col=1, val=1.0), (col=3, val=2.0)
426        assert_eq!(out_col_idx[0], 1);
427        assert!((out_values[0] - 1.0).abs() < 1e-6);
428        assert_eq!(out_col_idx[1], 3);
429        assert!((out_values[1] - 2.0).abs() < 1e-6);
430        // Row 1: (col=0, val=4.0)
431        assert_eq!(out_col_idx[2], 0);
432        assert!((out_values[2] - 4.0).abs() < 1e-6);
433        // Row 2: (col=1, val=7.0), (col=3, val=9.0)
434        assert_eq!(out_col_idx[3], 1);
435        assert!((out_values[3] - 7.0).abs() < 1e-6);
436        assert_eq!(out_col_idx[4], 3);
437        assert!((out_values[4] - 9.0).abs() < 1e-6);
438    }
439
440    #[test]
441    fn test_csr_to_ell_padding() {
442        // Verify ELL format pads correctly for rows of different lengths.
443        //
444        // 3x4 matrix with rows of lengths 1, 3, 2:
445        //   row 0: col 2 → val 5.0   (1 entry)
446        //   row 1: col 0, 1, 3 → 1, 2, 4   (3 entries, most dense row)
447        //   row 2: col 1, 3 → 3, 6   (2 entries)
448        //
449        // ELL max_nnz_per_row = 3 (the maximum over all rows)
450        // ELL is stored column-major: shape (rows, max_nnz_per_row) = (3, 3)
451        // Each column k of the ELL array lists entry k of every row.
452        let rows = 3usize;
453        let max_nnz_per_row = 3usize;
454        let ell_sentinel = -1i32;
455
456        // Build ELL col_idx (column-major, size = rows * max_nnz_per_row = 9)
457        // ELL column 0 (first entry of each row): row0→col2, row1→col0, row2→col1
458        // ELL column 1 (second entry):            row0→-1,   row1→col1, row2→col3
459        // ELL column 2 (third entry):             row0→-1,   row1→col3, row2→-1
460        let ell_col_idx = vec![
461            2i32,
462            0,
463            1, // ELL-col 0: row 0,1,2 first entry
464            ell_sentinel,
465            1,
466            3, // ELL-col 1: row 0 padded, row 1 & 2 second entry
467            ell_sentinel,
468            3,
469            ell_sentinel, // ELL-col 2: row 0 & 2 padded, row 1 third entry
470        ];
471        let ell_values = vec![
472            5.0f32, 1.0, 3.0, // ELL-col 0
473            0.0, 2.0, 6.0, // ELL-col 1
474            0.0, 4.0, 0.0, // ELL-col 2
475        ];
476
477        assert_eq!(ell_col_idx.len(), rows * max_nnz_per_row);
478        assert_eq!(ell_values.len(), rows * max_nnz_per_row);
479
480        // Count real entries per row (non-sentinel) and verify they match expected.
481        let expected_nnz_per_row = [1usize, 3, 2];
482        for r in 0..rows {
483            let count = (0..max_nnz_per_row)
484                .filter(|&k| ell_col_idx[k * rows + r] != ell_sentinel)
485                .count();
486            assert_eq!(
487                count, expected_nnz_per_row[r],
488                "row {r}: expected {} real entries, found {}",
489                expected_nnz_per_row[r], count
490            );
491        }
492
493        // Verify sentinel positions are zero-valued.
494        for idx in 0..ell_col_idx.len() {
495            if ell_col_idx[idx] == ell_sentinel {
496                assert!(
497                    ell_values[idx].abs() < 1e-10,
498                    "padded ELL value at index {idx} should be zero"
499                );
500            }
501        }
502
503        // Verify reconstruction: reading back non-sentinel entries for each row.
504        // Row 0: only entry is col=2, val=5.0
505        {
506            let r = 0usize;
507            let entries: Vec<(i32, f32)> = (0..max_nnz_per_row)
508                .filter_map(|k| {
509                    let c = ell_col_idx[k * rows + r];
510                    if c != ell_sentinel {
511                        Some((c, ell_values[k * rows + r]))
512                    } else {
513                        None
514                    }
515                })
516                .collect();
517            assert_eq!(entries, vec![(2, 5.0)]);
518        }
519        // Row 1: entries col=0,val=1; col=1,val=2; col=3,val=4
520        {
521            let r = 1usize;
522            let entries: Vec<(i32, f32)> = (0..max_nnz_per_row)
523                .filter_map(|k| {
524                    let c = ell_col_idx[k * rows + r];
525                    if c != ell_sentinel {
526                        Some((c, ell_values[k * rows + r]))
527                    } else {
528                        None
529                    }
530                })
531                .collect();
532            assert_eq!(entries, vec![(0, 1.0), (1, 2.0), (3, 4.0)]);
533        }
534    }
535}