1use oxicuda_blas::GpuFloat;
11use oxicuda_memory::DeviceBuffer;
12
13use crate::error::{SparseError, SparseResult};
14
15pub struct CsrMatrix<T: GpuFloat> {
20 rows: u32,
22 cols: u32,
24 nnz: u32,
26 row_ptr: DeviceBuffer<i32>,
29 col_idx: DeviceBuffer<i32>,
31 values: DeviceBuffer<T>,
33}
34
35impl<T: GpuFloat> CsrMatrix<T> {
36 pub fn from_host(
52 rows: u32,
53 cols: u32,
54 row_ptr: &[i32],
55 col_idx: &[i32],
56 values: &[T],
57 ) -> SparseResult<Self> {
58 if rows == 0 || cols == 0 {
60 return Err(SparseError::InvalidFormat(
61 "rows and cols must be non-zero".to_string(),
62 ));
63 }
64
65 let expected_row_ptr_len = rows as usize + 1;
67 if row_ptr.len() != expected_row_ptr_len {
68 return Err(SparseError::InvalidFormat(format!(
69 "row_ptr length ({}) must be rows + 1 ({})",
70 row_ptr.len(),
71 expected_row_ptr_len
72 )));
73 }
74
75 let nnz = values.len();
77 if nnz == 0 {
78 return Err(SparseError::ZeroNnz);
79 }
80 if col_idx.len() != nnz {
81 return Err(SparseError::InvalidFormat(format!(
82 "col_idx length ({}) must equal values length ({})",
83 col_idx.len(),
84 nnz
85 )));
86 }
87
88 if row_ptr[0] != 0 {
90 return Err(SparseError::InvalidFormat(
91 "row_ptr[0] must be 0".to_string(),
92 ));
93 }
94 if row_ptr[rows as usize] != nnz as i32 {
95 return Err(SparseError::InvalidFormat(format!(
96 "row_ptr[rows] ({}) must equal nnz ({})",
97 row_ptr[rows as usize], nnz
98 )));
99 }
100 for i in 0..rows as usize {
101 if row_ptr[i] > row_ptr[i + 1] {
102 return Err(SparseError::InvalidFormat(format!(
103 "row_ptr must be non-decreasing: row_ptr[{}]={} > row_ptr[{}]={}",
104 i,
105 row_ptr[i],
106 i + 1,
107 row_ptr[i + 1]
108 )));
109 }
110 }
111
112 let d_row_ptr = DeviceBuffer::from_host(row_ptr)?;
114 let d_col_idx = DeviceBuffer::from_host(col_idx)?;
115 let d_values = DeviceBuffer::from_host(values)?;
116
117 Ok(Self {
118 rows,
119 cols,
120 nnz: nnz as u32,
121 row_ptr: d_row_ptr,
122 col_idx: d_col_idx,
123 values: d_values,
124 })
125 }
126
127 pub fn from_device(
135 rows: u32,
136 cols: u32,
137 nnz: u32,
138 row_ptr: DeviceBuffer<i32>,
139 col_idx: DeviceBuffer<i32>,
140 values: DeviceBuffer<T>,
141 ) -> SparseResult<Self> {
142 if row_ptr.len() != (rows as usize + 1) {
143 return Err(SparseError::InvalidFormat(format!(
144 "row_ptr length ({}) must be rows + 1 ({})",
145 row_ptr.len(),
146 rows as usize + 1
147 )));
148 }
149 if col_idx.len() != nnz as usize {
150 return Err(SparseError::InvalidFormat(format!(
151 "col_idx length ({}) must equal nnz ({})",
152 col_idx.len(),
153 nnz
154 )));
155 }
156 if values.len() != nnz as usize {
157 return Err(SparseError::InvalidFormat(format!(
158 "values length ({}) must equal nnz ({})",
159 values.len(),
160 nnz
161 )));
162 }
163 Ok(Self {
164 rows,
165 cols,
166 nnz,
167 row_ptr,
168 col_idx,
169 values,
170 })
171 }
172
173 pub fn to_host(&self) -> SparseResult<(Vec<i32>, Vec<i32>, Vec<T>)> {
179 let mut h_row_ptr = vec![0i32; self.row_ptr.len()];
180 let mut h_col_idx = vec![0i32; self.col_idx.len()];
181 let mut h_values = vec![T::gpu_zero(); self.values.len()];
182
183 self.row_ptr.copy_to_host(&mut h_row_ptr)?;
184 self.col_idx.copy_to_host(&mut h_col_idx)?;
185 self.values.copy_to_host(&mut h_values)?;
186
187 Ok((h_row_ptr, h_col_idx, h_values))
188 }
189
190 #[inline]
192 pub fn rows(&self) -> u32 {
193 self.rows
194 }
195
196 #[inline]
198 pub fn cols(&self) -> u32 {
199 self.cols
200 }
201
202 #[inline]
204 pub fn nnz(&self) -> u32 {
205 self.nnz
206 }
207
208 #[inline]
210 pub fn density(&self) -> f64 {
211 let total = self.rows as f64 * self.cols as f64;
212 if total == 0.0 {
213 return 0.0;
214 }
215 self.nnz as f64 / total
216 }
217
218 #[inline]
224 pub fn is_valid(&self) -> bool {
225 self.row_ptr.len() == (self.rows as usize + 1)
226 && self.col_idx.len() == self.nnz as usize
227 && self.values.len() == self.nnz as usize
228 }
229
230 #[inline]
232 pub fn row_ptr(&self) -> &DeviceBuffer<i32> {
233 &self.row_ptr
234 }
235
236 #[inline]
238 pub fn col_idx(&self) -> &DeviceBuffer<i32> {
239 &self.col_idx
240 }
241
242 #[inline]
244 pub fn values(&self) -> &DeviceBuffer<T> {
245 &self.values
246 }
247
248 #[inline]
250 pub fn values_mut(&mut self) -> &mut DeviceBuffer<T> {
251 &mut self.values
252 }
253
254 #[inline]
256 pub fn avg_nnz_per_row(&self) -> f64 {
257 if self.rows == 0 {
258 return 0.0;
259 }
260 self.nnz as f64 / self.rows as f64
261 }
262
263 pub fn to_coo(&self) -> SparseResult<super::CooMatrix<T>> {
272 let (h_row_ptr, h_col_idx, h_values) = self.to_host()?;
273
274 let mut h_row_idx = Vec::with_capacity(self.nnz as usize);
276 for row in 0..self.rows {
277 let start = h_row_ptr[row as usize];
278 let end = h_row_ptr[row as usize + 1];
279 for _ in start..end {
280 h_row_idx.push(row as i32);
281 }
282 }
283
284 super::CooMatrix::from_host(self.rows, self.cols, &h_row_idx, &h_col_idx, &h_values)
285 }
286
287 pub fn to_csc(&self) -> SparseResult<super::CscMatrix<T>> {
295 let (h_row_ptr, h_col_idx, h_values) = self.to_host()?;
296
297 let mut col_counts = vec![0i32; self.cols as usize];
299 for &c in &h_col_idx {
300 col_counts[c as usize] += 1;
301 }
302
303 let mut h_col_ptr = vec![0i32; self.cols as usize + 1];
304 for i in 0..self.cols as usize {
305 h_col_ptr[i + 1] = h_col_ptr[i] + col_counts[i];
306 }
307
308 let mut h_csc_row_idx = vec![0i32; self.nnz as usize];
309 let mut h_csc_values = vec![T::gpu_zero(); self.nnz as usize];
310 let mut write_pos = h_col_ptr.clone();
311
312 for row in 0..self.rows as usize {
313 let start = h_row_ptr[row] as usize;
314 let end = h_row_ptr[row + 1] as usize;
315 for j in start..end {
316 let col = h_col_idx[j] as usize;
317 let dest = write_pos[col] as usize;
318 h_csc_row_idx[dest] = row as i32;
319 h_csc_values[dest] = h_values[j];
320 write_pos[col] += 1;
321 }
322 }
323
324 super::CscMatrix::from_host(
325 self.rows,
326 self.cols,
327 &h_col_ptr,
328 &h_csc_row_idx,
329 &h_csc_values,
330 )
331 }
332}
333
334#[cfg(test)]
335mod tests {
336 use super::*;
337
338 #[test]
339 fn csr_validation_row_ptr_length() {
340 let result = CsrMatrix::<f32>::from_host(3, 3, &[0, 2, 4], &[0, 1, 0, 2], &[1.0; 4]);
342 assert!(result.is_err());
343 }
344
345 #[test]
346 fn csr_validation_zero_nnz() {
347 let result = CsrMatrix::<f32>::from_host(2, 2, &[0, 0, 0], &[], &[]);
348 assert!(matches!(result, Err(SparseError::ZeroNnz)));
349 }
350
351 #[test]
352 fn csr_validation_mismatched_col_idx() {
353 let result = CsrMatrix::<f32>::from_host(2, 2, &[0, 1, 2], &[0], &[1.0, 2.0]);
354 assert!(result.is_err());
355 }
356
357 #[test]
358 fn csr_density() {
359 assert!((4.0_f64 / 16.0 - 0.25).abs() < 1e-10);
362 }
363}