1use oxicuda_blas::GpuFloat;
11use oxicuda_memory::DeviceBuffer;
12
13use crate::error::{SparseError, SparseResult};
14
15pub struct CooMatrix<T: GpuFloat> {
20 rows: u32,
22 cols: u32,
24 nnz: u32,
26 row_idx: DeviceBuffer<i32>,
28 col_idx: DeviceBuffer<i32>,
30 values: DeviceBuffer<T>,
32 sorted: bool,
34}
35
36impl<T: GpuFloat> CooMatrix<T> {
37 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 for (k, &r) in row_idx.iter().enumerate() {
80 if r < 0 || r as u32 >= rows {
81 return Err(SparseError::InvalidFormat(format!(
82 "row_idx[{k}] = {r} out of range [0, {rows})"
83 )));
84 }
85 }
86 for (k, &c) in col_idx.iter().enumerate() {
87 if c < 0 || c as u32 >= cols {
88 return Err(SparseError::InvalidFormat(format!(
89 "col_idx[{k}] = {c} out of range [0, {cols})"
90 )));
91 }
92 }
93
94 let d_row_idx = DeviceBuffer::from_host(row_idx)?;
95 let d_col_idx = DeviceBuffer::from_host(col_idx)?;
96 let d_values = DeviceBuffer::from_host(values)?;
97
98 Ok(Self {
99 rows,
100 cols,
101 nnz: nnz as u32,
102 row_idx: d_row_idx,
103 col_idx: d_col_idx,
104 values: d_values,
105 sorted: false,
106 })
107 }
108
109 pub fn from_device(
125 rows: u32,
126 cols: u32,
127 nnz: u32,
128 row_idx: DeviceBuffer<i32>,
129 col_idx: DeviceBuffer<i32>,
130 values: DeviceBuffer<T>,
131 ) -> SparseResult<Self> {
132 if row_idx.len() != nnz as usize
133 || col_idx.len() != nnz as usize
134 || values.len() != nnz as usize
135 {
136 return Err(SparseError::InvalidFormat(
137 "all arrays must have length equal to nnz".to_string(),
138 ));
139 }
140 Ok(Self {
141 rows,
142 cols,
143 nnz,
144 row_idx,
145 col_idx,
146 values,
147 sorted: false,
148 })
149 }
150
151 #[must_use]
156 pub fn with_sorted(mut self, sorted: bool) -> Self {
157 self.sorted = sorted;
158 self
159 }
160
161 pub fn to_host(&self) -> SparseResult<(Vec<i32>, Vec<i32>, Vec<T>)> {
167 let mut h_row_idx = vec![0i32; self.row_idx.len()];
168 let mut h_col_idx = vec![0i32; self.col_idx.len()];
169 let mut h_values = vec![T::gpu_zero(); self.values.len()];
170
171 self.row_idx.copy_to_host(&mut h_row_idx)?;
172 self.col_idx.copy_to_host(&mut h_col_idx)?;
173 self.values.copy_to_host(&mut h_values)?;
174
175 Ok((h_row_idx, h_col_idx, h_values))
176 }
177
178 pub fn to_csr(&self) -> SparseResult<super::CsrMatrix<T>> {
189 let (h_row_idx, h_col_idx, h_values) = self.to_host()?;
190
191 let mut row_counts = vec![0i32; self.rows as usize];
193 for &r in &h_row_idx {
194 if r < 0 || r as u32 >= self.rows {
195 return Err(SparseError::InvalidFormat(format!(
196 "row index {r} out of range for {} rows",
197 self.rows
198 )));
199 }
200 row_counts[r as usize] += 1;
201 }
202 for &c in &h_col_idx {
203 if c < 0 || c as u32 >= self.cols {
204 return Err(SparseError::InvalidFormat(format!(
205 "col index {c} out of range for {} cols",
206 self.cols
207 )));
208 }
209 }
210
211 let mut h_row_ptr = vec![0i32; self.rows as usize + 1];
212 for i in 0..self.rows as usize {
213 h_row_ptr[i + 1] = h_row_ptr[i] + row_counts[i];
214 }
215
216 let mut h_csr_col_idx = vec![0i32; self.nnz as usize];
218 let mut h_csr_values = vec![T::gpu_zero(); self.nnz as usize];
219 let mut write_pos = h_row_ptr.clone();
220
221 for i in 0..self.nnz as usize {
222 let row = h_row_idx[i] as usize;
223 let dest = write_pos[row] as usize;
224 h_csr_col_idx[dest] = h_col_idx[i];
225 h_csr_values[dest] = h_values[i];
226 write_pos[row] += 1;
227 }
228
229 super::CsrMatrix::from_host(
230 self.rows,
231 self.cols,
232 &h_row_ptr,
233 &h_csr_col_idx,
234 &h_csr_values,
235 )
236 }
237
238 pub fn to_csc(&self) -> SparseResult<super::CscMatrix<T>> {
249 let (h_row_idx, h_col_idx, h_values) = self.to_host()?;
250
251 for &r in &h_row_idx {
252 if r < 0 || r as u32 >= self.rows {
253 return Err(SparseError::InvalidFormat(format!(
254 "row index {r} out of range for {} rows",
255 self.rows
256 )));
257 }
258 }
259 let mut col_counts = vec![0i32; self.cols as usize];
260 for &c in &h_col_idx {
261 if c < 0 || c as u32 >= self.cols {
262 return Err(SparseError::InvalidFormat(format!(
263 "col index {c} out of range for {} cols",
264 self.cols
265 )));
266 }
267 col_counts[c as usize] += 1;
268 }
269
270 let mut h_col_ptr = vec![0i32; self.cols as usize + 1];
271 for i in 0..self.cols as usize {
272 h_col_ptr[i + 1] = h_col_ptr[i] + col_counts[i];
273 }
274
275 let mut h_csc_row_idx = vec![0i32; self.nnz as usize];
276 let mut h_csc_values = vec![T::gpu_zero(); self.nnz as usize];
277 let mut write_pos = h_col_ptr.clone();
278
279 for i in 0..self.nnz as usize {
280 let col = h_col_idx[i] as usize;
281 let dest = write_pos[col] as usize;
282 h_csc_row_idx[dest] = h_row_idx[i];
283 h_csc_values[dest] = h_values[i];
284 write_pos[col] += 1;
285 }
286
287 super::CscMatrix::from_host(
288 self.rows,
289 self.cols,
290 &h_col_ptr,
291 &h_csc_row_idx,
292 &h_csc_values,
293 )
294 }
295
296 #[inline]
298 pub fn is_sorted(&self) -> bool {
299 self.sorted
300 }
301
302 #[inline]
304 pub fn rows(&self) -> u32 {
305 self.rows
306 }
307
308 #[inline]
310 pub fn cols(&self) -> u32 {
311 self.cols
312 }
313
314 #[inline]
316 pub fn nnz(&self) -> u32 {
317 self.nnz
318 }
319
320 #[inline]
322 pub fn row_idx(&self) -> &DeviceBuffer<i32> {
323 &self.row_idx
324 }
325
326 #[inline]
328 pub fn col_idx(&self) -> &DeviceBuffer<i32> {
329 &self.col_idx
330 }
331
332 #[inline]
334 pub fn values(&self) -> &DeviceBuffer<T> {
335 &self.values
336 }
337}
338
339#[cfg(test)]
340mod tests {
341 use super::*;
342
343 #[test]
344 fn coo_validation_mismatched_lengths() {
345 let result = CooMatrix::<f32>::from_host(3, 3, &[0, 1], &[0, 1, 2], &[1.0; 3]);
346 assert!(result.is_err());
347 }
348
349 #[test]
350 fn coo_validation_zero_nnz() {
351 let result = CooMatrix::<f32>::from_host(2, 2, &[], &[], &[]);
352 assert!(matches!(result, Err(SparseError::ZeroNnz)));
353 }
354
355 #[test]
356 fn coo_sorted_flag() {
357 }
359
360 #[test]
361 fn coo_validation_row_idx_out_of_range() {
362 let result = CooMatrix::<f32>::from_host(2, 2, &[0, 2], &[0, 1], &[1.0, 1.0]);
364 assert!(matches!(result, Err(SparseError::InvalidFormat(_))));
365 }
366
367 #[test]
368 fn coo_validation_col_idx_out_of_range() {
369 let result = CooMatrix::<f32>::from_host(2, 2, &[0, 1], &[0, -1], &[1.0, 1.0]);
371 assert!(matches!(result, Err(SparseError::InvalidFormat(_))));
372 }
373}
374
375#[cfg(all(test, feature = "gpu-tests"))]
381mod gpu_device_tests {
382 use super::*;
383 use crate::gpu_test_support::gpu_handle;
384
385 #[test]
390 fn to_csr_rejects_out_of_range_row_idx_from_device() {
391 let Some(_handle) = gpu_handle() else {
395 return;
396 };
397 let d_row_idx = DeviceBuffer::from_host(&[0i32, 5]).expect("test: upload row_idx");
398 let d_col_idx = DeviceBuffer::from_host(&[0i32, 1]).expect("test: upload col_idx");
399 let d_values = DeviceBuffer::from_host(&[1.0f32, 1.0]).expect("test: upload values");
400 let coo = CooMatrix::<f32>::from_device(2, 2, 2, d_row_idx, d_col_idx, d_values)
401 .expect("test: build COO from device buffers (lengths are consistent)");
402
403 let result = coo.to_csr();
404 assert!(matches!(result, Err(SparseError::InvalidFormat(_))));
405 }
406
407 #[test]
408 fn to_csc_rejects_out_of_range_col_idx_from_device() {
409 let Some(_handle) = gpu_handle() else {
410 return;
411 };
412 let d_row_idx = DeviceBuffer::from_host(&[0i32, 1]).expect("test: upload row_idx");
413 let d_col_idx = DeviceBuffer::from_host(&[0i32, 5]).expect("test: upload col_idx");
414 let d_values = DeviceBuffer::from_host(&[1.0f32, 1.0]).expect("test: upload values");
415 let coo = CooMatrix::<f32>::from_device(2, 2, 2, d_row_idx, d_col_idx, d_values)
416 .expect("test: build COO from device buffers (lengths are consistent)");
417
418 let result = coo.to_csc();
419 assert!(matches!(result, Err(SparseError::InvalidFormat(_))));
420 }
421}