Skip to main content

oxicuda_sparse/format/
coo.rs

1//! Coordinate (COO) format.
2//!
3//! COO stores each non-zero as a `(row, col, value)` triplet. It is the
4//! simplest sparse format and is commonly used as an intermediate format
5//! for assembly before converting to CSR or CSC.
6//!
7//! The triplets can be in any order, but sorted COO enables more efficient
8//! conversion to CSR.
9
10use oxicuda_blas::GpuFloat;
11use oxicuda_memory::DeviceBuffer;
12
13use crate::error::{SparseError, SparseResult};
14
15/// A sparse matrix in Coordinate (COO) format, stored on GPU.
16///
17/// The matrix has shape `(rows, cols)` with `nnz` non-zero elements.
18/// Each non-zero is represented by its row index, column index, and value.
19pub struct CooMatrix<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    /// Row indices of length `nnz`.
27    row_idx: DeviceBuffer<i32>,
28    /// Column indices of length `nnz`.
29    col_idx: DeviceBuffer<i32>,
30    /// Non-zero values of length `nnz`.
31    values: DeviceBuffer<T>,
32    /// Whether the triplets are sorted by (row, col).
33    sorted: bool,
34}
35
36impl<T: GpuFloat> CooMatrix<T> {
37    /// Creates a COO matrix from host-side arrays, uploading to GPU.
38    ///
39    /// The triplets are assumed to be unsorted unless the caller knows
40    /// otherwise (use [`with_sorted`](Self::with_sorted) to override).
41    ///
42    /// # Arguments
43    ///
44    /// * `rows` -- Number of rows.
45    /// * `cols` -- Number of columns.
46    /// * `row_idx` -- Row indices of length `nnz`.
47    /// * `col_idx` -- Column indices of length `nnz`.
48    /// * `values` -- Non-zero values of length `nnz`.
49    ///
50    /// # Errors
51    ///
52    /// Returns [`SparseError::InvalidFormat`] if array lengths are inconsistent.
53    pub fn from_host(
54        rows: u32,
55        cols: u32,
56        row_idx: &[i32],
57        col_idx: &[i32],
58        values: &[T],
59    ) -> SparseResult<Self> {
60        if rows == 0 || cols == 0 {
61            return Err(SparseError::InvalidFormat(
62                "rows and cols must be non-zero".to_string(),
63            ));
64        }
65
66        let nnz = values.len();
67        if nnz == 0 {
68            return Err(SparseError::ZeroNnz);
69        }
70        if row_idx.len() != nnz || col_idx.len() != nnz {
71            return Err(SparseError::InvalidFormat(format!(
72                "row_idx ({}), col_idx ({}), and values ({}) must have equal length",
73                row_idx.len(),
74                col_idx.len(),
75                nnz
76            )));
77        }
78
79        let d_row_idx = DeviceBuffer::from_host(row_idx)?;
80        let d_col_idx = DeviceBuffer::from_host(col_idx)?;
81        let d_values = DeviceBuffer::from_host(values)?;
82
83        Ok(Self {
84            rows,
85            cols,
86            nnz: nnz as u32,
87            row_idx: d_row_idx,
88            col_idx: d_col_idx,
89            values: d_values,
90            sorted: false,
91        })
92    }
93
94    /// Creates a COO matrix from pre-allocated device buffers.
95    ///
96    /// # Errors
97    ///
98    /// Returns [`SparseError::InvalidFormat`] if buffer lengths are inconsistent.
99    pub fn from_device(
100        rows: u32,
101        cols: u32,
102        nnz: u32,
103        row_idx: DeviceBuffer<i32>,
104        col_idx: DeviceBuffer<i32>,
105        values: DeviceBuffer<T>,
106    ) -> SparseResult<Self> {
107        if row_idx.len() != nnz as usize
108            || col_idx.len() != nnz as usize
109            || values.len() != nnz as usize
110        {
111            return Err(SparseError::InvalidFormat(
112                "all arrays must have length equal to nnz".to_string(),
113            ));
114        }
115        Ok(Self {
116            rows,
117            cols,
118            nnz,
119            row_idx,
120            col_idx,
121            values,
122            sorted: false,
123        })
124    }
125
126    /// Mark the COO matrix as sorted by (row, col).
127    ///
128    /// This is a hint for conversion routines; the caller is responsible
129    /// for ensuring correctness.
130    #[must_use]
131    pub fn with_sorted(mut self, sorted: bool) -> Self {
132        self.sorted = sorted;
133        self
134    }
135
136    /// Downloads the COO arrays from GPU to host memory.
137    ///
138    /// # Errors
139    ///
140    /// Returns [`SparseError::Cuda`] on transfer failure.
141    pub fn to_host(&self) -> SparseResult<(Vec<i32>, Vec<i32>, Vec<T>)> {
142        let mut h_row_idx = vec![0i32; self.row_idx.len()];
143        let mut h_col_idx = vec![0i32; self.col_idx.len()];
144        let mut h_values = vec![T::gpu_zero(); self.values.len()];
145
146        self.row_idx.copy_to_host(&mut h_row_idx)?;
147        self.col_idx.copy_to_host(&mut h_col_idx)?;
148        self.values.copy_to_host(&mut h_values)?;
149
150        Ok((h_row_idx, h_col_idx, h_values))
151    }
152
153    /// Converts this COO matrix to CSR format.
154    ///
155    /// Downloads to host, builds row pointers via histogram, then uploads.
156    ///
157    /// # Errors
158    ///
159    /// Returns [`SparseError::Cuda`] on transfer failure.
160    pub fn to_csr(&self) -> SparseResult<super::CsrMatrix<T>> {
161        let (h_row_idx, h_col_idx, h_values) = self.to_host()?;
162
163        // Build row pointers from row indices
164        let mut row_counts = vec![0i32; self.rows as usize];
165        for &r in &h_row_idx {
166            row_counts[r as usize] += 1;
167        }
168
169        let mut h_row_ptr = vec![0i32; self.rows as usize + 1];
170        for i in 0..self.rows as usize {
171            h_row_ptr[i + 1] = h_row_ptr[i] + row_counts[i];
172        }
173
174        // Place entries in row order
175        let mut h_csr_col_idx = vec![0i32; self.nnz as usize];
176        let mut h_csr_values = vec![T::gpu_zero(); self.nnz as usize];
177        let mut write_pos = h_row_ptr.clone();
178
179        for i in 0..self.nnz as usize {
180            let row = h_row_idx[i] as usize;
181            let dest = write_pos[row] as usize;
182            h_csr_col_idx[dest] = h_col_idx[i];
183            h_csr_values[dest] = h_values[i];
184            write_pos[row] += 1;
185        }
186
187        super::CsrMatrix::from_host(
188            self.rows,
189            self.cols,
190            &h_row_ptr,
191            &h_csr_col_idx,
192            &h_csr_values,
193        )
194    }
195
196    /// Converts this COO matrix to CSC format.
197    ///
198    /// Downloads to host, builds column pointers, then uploads.
199    ///
200    /// # Errors
201    ///
202    /// Returns [`SparseError::Cuda`] on transfer failure.
203    pub fn to_csc(&self) -> SparseResult<super::CscMatrix<T>> {
204        let (h_row_idx, h_col_idx, h_values) = self.to_host()?;
205
206        let mut col_counts = vec![0i32; self.cols as usize];
207        for &c in &h_col_idx {
208            col_counts[c as usize] += 1;
209        }
210
211        let mut h_col_ptr = vec![0i32; self.cols as usize + 1];
212        for i in 0..self.cols as usize {
213            h_col_ptr[i + 1] = h_col_ptr[i] + col_counts[i];
214        }
215
216        let mut h_csc_row_idx = vec![0i32; self.nnz as usize];
217        let mut h_csc_values = vec![T::gpu_zero(); self.nnz as usize];
218        let mut write_pos = h_col_ptr.clone();
219
220        for i in 0..self.nnz as usize {
221            let col = h_col_idx[i] as usize;
222            let dest = write_pos[col] as usize;
223            h_csc_row_idx[dest] = h_row_idx[i];
224            h_csc_values[dest] = h_values[i];
225            write_pos[col] += 1;
226        }
227
228        super::CscMatrix::from_host(
229            self.rows,
230            self.cols,
231            &h_col_ptr,
232            &h_csc_row_idx,
233            &h_csc_values,
234        )
235    }
236
237    /// Returns whether the triplets are sorted by (row, col).
238    #[inline]
239    pub fn is_sorted(&self) -> bool {
240        self.sorted
241    }
242
243    /// Returns the number of rows.
244    #[inline]
245    pub fn rows(&self) -> u32 {
246        self.rows
247    }
248
249    /// Returns the number of columns.
250    #[inline]
251    pub fn cols(&self) -> u32 {
252        self.cols
253    }
254
255    /// Returns the number of non-zero elements.
256    #[inline]
257    pub fn nnz(&self) -> u32 {
258        self.nnz
259    }
260
261    /// Returns a reference to the row index device buffer.
262    #[inline]
263    pub fn row_idx(&self) -> &DeviceBuffer<i32> {
264        &self.row_idx
265    }
266
267    /// Returns a reference to the column index device buffer.
268    #[inline]
269    pub fn col_idx(&self) -> &DeviceBuffer<i32> {
270        &self.col_idx
271    }
272
273    /// Returns a reference to the values device buffer.
274    #[inline]
275    pub fn values(&self) -> &DeviceBuffer<T> {
276        &self.values
277    }
278}
279
280#[cfg(test)]
281mod tests {
282    use super::*;
283
284    #[test]
285    fn coo_validation_mismatched_lengths() {
286        let result = CooMatrix::<f32>::from_host(3, 3, &[0, 1], &[0, 1, 2], &[1.0; 3]);
287        assert!(result.is_err());
288    }
289
290    #[test]
291    fn coo_validation_zero_nnz() {
292        let result = CooMatrix::<f32>::from_host(2, 2, &[], &[], &[]);
293        assert!(matches!(result, Err(SparseError::ZeroNnz)));
294    }
295
296    #[test]
297    fn coo_sorted_flag() {
298        // Just verify the flag API works (no GPU needed)
299    }
300}