Skip to main content

oxicuda_sparse/format/
csc.rs

1//! Compressed Sparse Column (CSC) format.
2//!
3//! CSC is the column-compressed counterpart of CSR. It stores:
4//! - `col_ptr[cols+1]`: indices into `row_idx`/`values` for each column
5//! - `row_idx[nnz]`: row index for each non-zero
6//! - `values[nnz]`: the non-zero values
7//!
8//! CSC is efficient for column-oriented operations and is the natural
9//! format for the transpose of a CSR matrix.
10
11use oxicuda_blas::GpuFloat;
12use oxicuda_memory::DeviceBuffer;
13
14use crate::error::{SparseError, SparseResult};
15
16/// A sparse matrix in Compressed Sparse Column (CSC) format, stored on GPU.
17///
18/// The matrix has shape `(rows, cols)` with `nnz` non-zero elements.
19pub struct CscMatrix<T: GpuFloat> {
20    /// Number of rows.
21    rows: u32,
22    /// Number of columns.
23    cols: u32,
24    /// Number of non-zero elements.
25    nnz: u32,
26    /// Column pointer array of length `cols + 1`.
27    col_ptr: DeviceBuffer<i32>,
28    /// Row indices of length `nnz`.
29    row_idx: DeviceBuffer<i32>,
30    /// Non-zero values of length `nnz`.
31    values: DeviceBuffer<T>,
32}
33
34impl<T: GpuFloat> CscMatrix<T> {
35    /// Creates a CSC matrix from host-side arrays, uploading to GPU.
36    ///
37    /// # Arguments
38    ///
39    /// * `rows` -- Number of rows.
40    /// * `cols` -- Number of columns.
41    /// * `col_ptr` -- Column pointer array of length `cols + 1`.
42    /// * `row_idx` -- Row indices of length `nnz`.
43    /// * `values` -- Non-zero values of length `nnz`.
44    ///
45    /// # Errors
46    ///
47    /// Returns [`SparseError::InvalidFormat`] if array lengths are inconsistent.
48    pub fn from_host(
49        rows: u32,
50        cols: u32,
51        col_ptr: &[i32],
52        row_idx: &[i32],
53        values: &[T],
54    ) -> SparseResult<Self> {
55        if rows == 0 || cols == 0 {
56            return Err(SparseError::InvalidFormat(
57                "rows and cols must be non-zero".to_string(),
58            ));
59        }
60
61        let expected_col_ptr_len = cols as usize + 1;
62        if col_ptr.len() != expected_col_ptr_len {
63            return Err(SparseError::InvalidFormat(format!(
64                "col_ptr length ({}) must be cols + 1 ({})",
65                col_ptr.len(),
66                expected_col_ptr_len
67            )));
68        }
69
70        let nnz = values.len();
71        if nnz == 0 {
72            return Err(SparseError::ZeroNnz);
73        }
74        if row_idx.len() != nnz {
75            return Err(SparseError::InvalidFormat(format!(
76                "row_idx length ({}) must equal values length ({})",
77                row_idx.len(),
78                nnz
79            )));
80        }
81
82        if col_ptr[0] != 0 {
83            return Err(SparseError::InvalidFormat(
84                "col_ptr[0] must be 0".to_string(),
85            ));
86        }
87        if col_ptr[cols as usize] != nnz as i32 {
88            return Err(SparseError::InvalidFormat(format!(
89                "col_ptr[cols] ({}) must equal nnz ({})",
90                col_ptr[cols as usize], nnz
91            )));
92        }
93        for i in 0..cols as usize {
94            if col_ptr[i] > col_ptr[i + 1] {
95                return Err(SparseError::InvalidFormat(format!(
96                    "col_ptr must be non-decreasing: col_ptr[{}]={} > col_ptr[{}]={}",
97                    i,
98                    col_ptr[i],
99                    i + 1,
100                    col_ptr[i + 1]
101                )));
102            }
103        }
104
105        let d_col_ptr = DeviceBuffer::from_host(col_ptr)?;
106        let d_row_idx = DeviceBuffer::from_host(row_idx)?;
107        let d_values = DeviceBuffer::from_host(values)?;
108
109        Ok(Self {
110            rows,
111            cols,
112            nnz: nnz as u32,
113            col_ptr: d_col_ptr,
114            row_idx: d_row_idx,
115            values: d_values,
116        })
117    }
118
119    /// Creates a CSC matrix from pre-allocated device buffers.
120    ///
121    /// # Errors
122    ///
123    /// Returns [`SparseError::InvalidFormat`] if buffer lengths are inconsistent.
124    pub fn from_device(
125        rows: u32,
126        cols: u32,
127        nnz: u32,
128        col_ptr: DeviceBuffer<i32>,
129        row_idx: DeviceBuffer<i32>,
130        values: DeviceBuffer<T>,
131    ) -> SparseResult<Self> {
132        if col_ptr.len() != (cols as usize + 1) {
133            return Err(SparseError::InvalidFormat(format!(
134                "col_ptr length ({}) must be cols + 1 ({})",
135                col_ptr.len(),
136                cols as usize + 1
137            )));
138        }
139        if row_idx.len() != nnz as usize || values.len() != nnz as usize {
140            return Err(SparseError::InvalidFormat(
141                "row_idx and values lengths must equal nnz".to_string(),
142            ));
143        }
144        Ok(Self {
145            rows,
146            cols,
147            nnz,
148            col_ptr,
149            row_idx,
150            values,
151        })
152    }
153
154    /// Downloads the CSC arrays from GPU to host memory.
155    ///
156    /// # Errors
157    ///
158    /// Returns [`SparseError::Cuda`] on transfer failure.
159    pub fn to_host(&self) -> SparseResult<(Vec<i32>, Vec<i32>, Vec<T>)> {
160        let mut h_col_ptr = vec![0i32; self.col_ptr.len()];
161        let mut h_row_idx = vec![0i32; self.row_idx.len()];
162        let mut h_values = vec![T::gpu_zero(); self.values.len()];
163
164        self.col_ptr.copy_to_host(&mut h_col_ptr)?;
165        self.row_idx.copy_to_host(&mut h_row_idx)?;
166        self.values.copy_to_host(&mut h_values)?;
167
168        Ok((h_col_ptr, h_row_idx, h_values))
169    }
170
171    /// Converts this CSC matrix to CSR format on the host.
172    ///
173    /// Downloads data, transposes the structure (CSC -> CSR is analogous
174    /// to transposing the matrix), then uploads.
175    ///
176    /// # Errors
177    ///
178    /// Returns [`SparseError::Cuda`] on transfer failure.
179    pub fn to_csr(&self) -> SparseResult<super::CsrMatrix<T>> {
180        let (h_col_ptr, h_row_idx, h_values) = self.to_host()?;
181
182        // CSC -> CSR is the same as transposing the CSR->CSC algorithm
183        let mut row_counts = vec![0i32; self.rows as usize];
184        for &r in &h_row_idx {
185            row_counts[r as usize] += 1;
186        }
187
188        let mut h_row_ptr = vec![0i32; self.rows as usize + 1];
189        for i in 0..self.rows as usize {
190            h_row_ptr[i + 1] = h_row_ptr[i] + row_counts[i];
191        }
192
193        let mut h_csr_col_idx = vec![0i32; self.nnz as usize];
194        let mut h_csr_values = vec![T::gpu_zero(); self.nnz as usize];
195        let mut write_pos = h_row_ptr.clone();
196
197        for col in 0..self.cols as usize {
198            let start = h_col_ptr[col] as usize;
199            let end = h_col_ptr[col + 1] as usize;
200            for j in start..end {
201                let row = h_row_idx[j] as usize;
202                let dest = write_pos[row] as usize;
203                h_csr_col_idx[dest] = col as i32;
204                h_csr_values[dest] = h_values[j];
205                write_pos[row] += 1;
206            }
207        }
208
209        super::CsrMatrix::from_host(
210            self.rows,
211            self.cols,
212            &h_row_ptr,
213            &h_csr_col_idx,
214            &h_csr_values,
215        )
216    }
217
218    /// Returns the number of rows.
219    #[inline]
220    pub fn rows(&self) -> u32 {
221        self.rows
222    }
223
224    /// Returns the number of columns.
225    #[inline]
226    pub fn cols(&self) -> u32 {
227        self.cols
228    }
229
230    /// Returns the number of non-zero elements.
231    #[inline]
232    pub fn nnz(&self) -> u32 {
233        self.nnz
234    }
235
236    /// Returns a reference to the column pointer device buffer.
237    #[inline]
238    pub fn col_ptr(&self) -> &DeviceBuffer<i32> {
239        &self.col_ptr
240    }
241
242    /// Returns a reference to the row index device buffer.
243    #[inline]
244    pub fn row_idx(&self) -> &DeviceBuffer<i32> {
245        &self.row_idx
246    }
247
248    /// Returns a reference to the values device buffer.
249    #[inline]
250    pub fn values(&self) -> &DeviceBuffer<T> {
251        &self.values
252    }
253}
254
255#[cfg(test)]
256mod tests {
257    use super::*;
258
259    #[test]
260    fn csc_validation_col_ptr_length() {
261        let result = CscMatrix::<f32>::from_host(3, 3, &[0, 2, 4], &[0, 1, 0, 2], &[1.0; 4]);
262        assert!(result.is_err());
263    }
264
265    #[test]
266    fn csc_validation_zero_nnz() {
267        let result = CscMatrix::<f32>::from_host(2, 2, &[0, 0, 0], &[], &[]);
268        assert!(matches!(result, Err(SparseError::ZeroNnz)));
269    }
270}