1use oxicuda_blas::GpuFloat;
12use oxicuda_memory::DeviceBuffer;
13
14use crate::error::{SparseError, SparseResult};
15
16pub struct CscMatrix<T: GpuFloat> {
20 rows: u32,
22 cols: u32,
24 nnz: u32,
26 col_ptr: DeviceBuffer<i32>,
28 row_idx: DeviceBuffer<i32>,
30 values: DeviceBuffer<T>,
32}
33
34impl<T: GpuFloat> CscMatrix<T> {
35 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 for (k, &r) in row_idx.iter().enumerate() {
109 if r < 0 || r as u32 >= rows {
110 return Err(SparseError::InvalidFormat(format!(
111 "row_idx[{k}] = {r} out of range [0, {rows})"
112 )));
113 }
114 }
115
116 let d_col_ptr = DeviceBuffer::from_host(col_ptr)?;
117 let d_row_idx = DeviceBuffer::from_host(row_idx)?;
118 let d_values = DeviceBuffer::from_host(values)?;
119
120 Ok(Self {
121 rows,
122 cols,
123 nnz: nnz as u32,
124 col_ptr: d_col_ptr,
125 row_idx: d_row_idx,
126 values: d_values,
127 })
128 }
129
130 pub fn from_device(
136 rows: u32,
137 cols: u32,
138 nnz: u32,
139 col_ptr: DeviceBuffer<i32>,
140 row_idx: DeviceBuffer<i32>,
141 values: DeviceBuffer<T>,
142 ) -> SparseResult<Self> {
143 if col_ptr.len() != (cols as usize + 1) {
144 return Err(SparseError::InvalidFormat(format!(
145 "col_ptr length ({}) must be cols + 1 ({})",
146 col_ptr.len(),
147 cols as usize + 1
148 )));
149 }
150 if row_idx.len() != nnz as usize || values.len() != nnz as usize {
151 return Err(SparseError::InvalidFormat(
152 "row_idx and values lengths must equal nnz".to_string(),
153 ));
154 }
155 Ok(Self {
156 rows,
157 cols,
158 nnz,
159 col_ptr,
160 row_idx,
161 values,
162 })
163 }
164
165 pub fn to_host(&self) -> SparseResult<(Vec<i32>, Vec<i32>, Vec<T>)> {
171 let mut h_col_ptr = vec![0i32; self.col_ptr.len()];
172 let mut h_row_idx = vec![0i32; self.row_idx.len()];
173 let mut h_values = vec![T::gpu_zero(); self.values.len()];
174
175 self.col_ptr.copy_to_host(&mut h_col_ptr)?;
176 self.row_idx.copy_to_host(&mut h_row_idx)?;
177 self.values.copy_to_host(&mut h_values)?;
178
179 Ok((h_col_ptr, h_row_idx, h_values))
180 }
181
182 pub fn to_csr(&self) -> SparseResult<super::CsrMatrix<T>> {
191 let (h_col_ptr, h_row_idx, h_values) = self.to_host()?;
192
193 let mut row_counts = vec![0i32; self.rows as usize];
195 for &r in &h_row_idx {
196 row_counts[r as usize] += 1;
197 }
198
199 let mut h_row_ptr = vec![0i32; self.rows as usize + 1];
200 for i in 0..self.rows as usize {
201 h_row_ptr[i + 1] = h_row_ptr[i] + row_counts[i];
202 }
203
204 let mut h_csr_col_idx = vec![0i32; self.nnz as usize];
205 let mut h_csr_values = vec![T::gpu_zero(); self.nnz as usize];
206 let mut write_pos = h_row_ptr.clone();
207
208 for col in 0..self.cols as usize {
209 let start = h_col_ptr[col] as usize;
210 let end = h_col_ptr[col + 1] as usize;
211 for j in start..end {
212 let row = h_row_idx[j] as usize;
213 let dest = write_pos[row] as usize;
214 h_csr_col_idx[dest] = col as i32;
215 h_csr_values[dest] = h_values[j];
216 write_pos[row] += 1;
217 }
218 }
219
220 super::CsrMatrix::from_host(
221 self.rows,
222 self.cols,
223 &h_row_ptr,
224 &h_csr_col_idx,
225 &h_csr_values,
226 )
227 }
228
229 #[inline]
231 pub fn rows(&self) -> u32 {
232 self.rows
233 }
234
235 #[inline]
237 pub fn cols(&self) -> u32 {
238 self.cols
239 }
240
241 #[inline]
243 pub fn nnz(&self) -> u32 {
244 self.nnz
245 }
246
247 #[inline]
249 pub fn col_ptr(&self) -> &DeviceBuffer<i32> {
250 &self.col_ptr
251 }
252
253 #[inline]
255 pub fn row_idx(&self) -> &DeviceBuffer<i32> {
256 &self.row_idx
257 }
258
259 #[inline]
261 pub fn values(&self) -> &DeviceBuffer<T> {
262 &self.values
263 }
264}
265
266#[cfg(test)]
267mod tests {
268 use super::*;
269
270 #[test]
271 fn csc_validation_col_ptr_length() {
272 let result = CscMatrix::<f32>::from_host(3, 3, &[0, 2, 4], &[0, 1, 0, 2], &[1.0; 4]);
273 assert!(result.is_err());
274 }
275
276 #[test]
277 fn csc_validation_zero_nnz() {
278 let result = CscMatrix::<f32>::from_host(2, 2, &[0, 0, 0], &[], &[]);
279 assert!(matches!(result, Err(SparseError::ZeroNnz)));
280 }
281
282 #[test]
283 fn csc_validation_row_idx_out_of_range() {
284 let result = CscMatrix::<f32>::from_host(2, 2, &[0, 1, 2], &[0, 2], &[1.0, 2.0]);
286 assert!(matches!(result, Err(SparseError::InvalidFormat(_))));
287 }
288
289 #[test]
290 fn csc_validation_negative_row_idx() {
291 let result = CscMatrix::<f32>::from_host(2, 2, &[0, 1, 2], &[0, -1], &[1.0, 2.0]);
292 assert!(matches!(result, Err(SparseError::InvalidFormat(_))));
293 }
294}