1use oxicuda_blas::GpuFloat;
22
23use crate::error::{SparseError, SparseResult};
24use crate::format::CsrMatrix;
25
26#[derive(Debug, Clone, PartialEq)]
33pub struct HostCsr {
34 pub nrows: usize,
36 pub ncols: usize,
38 pub row_ptr: Vec<usize>,
40 pub col_indices: Vec<usize>,
42 pub values: Vec<f64>,
44}
45
46impl HostCsr {
47 pub fn new(
55 nrows: usize,
56 ncols: usize,
57 row_ptr: Vec<usize>,
58 col_indices: Vec<usize>,
59 values: Vec<f64>,
60 ) -> SparseResult<Self> {
61 if row_ptr.len() != nrows + 1 {
62 return Err(SparseError::InvalidFormat(format!(
63 "row_ptr length ({}) must be nrows + 1 ({})",
64 row_ptr.len(),
65 nrows + 1
66 )));
67 }
68 if col_indices.len() != values.len() {
69 return Err(SparseError::InvalidFormat(format!(
70 "col_indices length ({}) must equal values length ({})",
71 col_indices.len(),
72 values.len()
73 )));
74 }
75 if !row_ptr.is_empty() && row_ptr[0] != 0 {
76 return Err(SparseError::InvalidFormat(
77 "row_ptr[0] must be 0".to_string(),
78 ));
79 }
80 if let Some(&last) = row_ptr.last() {
81 if last != values.len() {
82 return Err(SparseError::InvalidFormat(format!(
83 "row_ptr[nrows] ({}) must equal nnz ({})",
84 last,
85 values.len()
86 )));
87 }
88 }
89 for i in 0..nrows {
90 if row_ptr[i] > row_ptr[i + 1] {
91 return Err(SparseError::InvalidFormat(
92 "row_ptr must be non-decreasing".to_string(),
93 ));
94 }
95 }
96 for &c in &col_indices {
97 if c >= ncols {
98 return Err(SparseError::InvalidFormat(format!(
99 "column index {c} out of range (ncols = {ncols})"
100 )));
101 }
102 }
103 Ok(Self {
104 nrows,
105 ncols,
106 row_ptr,
107 col_indices,
108 values,
109 })
110 }
111
112 pub fn from_gpu<T: GpuFloat>(matrix: &CsrMatrix<T>) -> SparseResult<Self> {
121 let (h_row_ptr, h_col_idx, h_values) = matrix.to_host()?;
122 let nrows = matrix.rows() as usize;
123 let ncols = matrix.cols() as usize;
124
125 let mut row_ptr = vec![0usize; nrows + 1];
126 let mut col_indices = Vec::with_capacity(h_col_idx.len());
127 let mut values = Vec::with_capacity(h_values.len());
128
129 for i in 0..nrows {
130 let start = h_row_ptr[i] as usize;
131 let end = h_row_ptr[i + 1] as usize;
132 let mut entries: Vec<(usize, f64)> = (start..end)
134 .map(|k| (h_col_idx[k] as usize, gpu_to_f64(h_values[k])))
135 .collect();
136 entries.sort_by_key(|&(c, _)| c);
137 for (c, v) in entries {
138 col_indices.push(c);
139 values.push(v);
140 }
141 row_ptr[i + 1] = col_indices.len();
142 }
143
144 Self::new(nrows, ncols, row_ptr, col_indices, values)
145 }
146
147 pub fn to_gpu<T: GpuFloat>(&self) -> SparseResult<CsrMatrix<T>> {
156 if self.values.is_empty() {
157 return Err(SparseError::ZeroNnz);
158 }
159 let row_ptr: Vec<i32> = self.row_ptr.iter().map(|&x| x as i32).collect();
160 let col_idx: Vec<i32> = self.col_indices.iter().map(|&x| x as i32).collect();
161 let values: Vec<T> = self.values.iter().map(|&v| f64_to_gpu::<T>(v)).collect();
162 CsrMatrix::from_host(
163 self.nrows as u32,
164 self.ncols as u32,
165 &row_ptr,
166 &col_idx,
167 &values,
168 )
169 }
170
171 #[inline]
173 pub fn nnz(&self) -> usize {
174 self.values.len()
175 }
176
177 pub fn get(&self, row: usize, col: usize) -> Option<f64> {
179 let start = self.row_ptr[row];
180 let end = self.row_ptr[row + 1];
181 match self.col_indices[start..end].binary_search(&col) {
183 Ok(pos) => Some(self.values[start + pos]),
184 Err(_) => None,
185 }
186 }
187
188 pub fn diagonal(&self) -> Vec<f64> {
190 let n = self.nrows.min(self.ncols);
191 let mut diag = vec![0.0f64; n];
192 for (i, slot) in diag.iter_mut().enumerate() {
193 if let Some(v) = self.get(i, i) {
194 *slot = v;
195 }
196 }
197 diag
198 }
199
200 pub fn matvec(&self, x: &[f64]) -> Vec<f64> {
205 let mut y = vec![0.0f64; self.nrows];
206 for (i, yi) in y.iter_mut().enumerate() {
207 let start = self.row_ptr[i];
208 let end = self.row_ptr[i + 1];
209 let mut acc = 0.0f64;
210 for k in start..end {
211 acc += self.values[k] * x[self.col_indices[k]];
212 }
213 *yi = acc;
214 }
215 y
216 }
217
218 pub fn transpose(&self) -> HostCsr {
222 let mut col_counts = vec![0usize; self.ncols];
223 for &c in &self.col_indices {
224 col_counts[c] += 1;
225 }
226 let mut t_row_ptr = vec![0usize; self.ncols + 1];
227 for j in 0..self.ncols {
228 t_row_ptr[j + 1] = t_row_ptr[j] + col_counts[j];
229 }
230 let nnz = self.values.len();
231 let mut t_col_indices = vec![0usize; nnz];
232 let mut t_values = vec![0.0f64; nnz];
233 let mut write_pos = t_row_ptr.clone();
234 for i in 0..self.nrows {
235 let start = self.row_ptr[i];
236 let end = self.row_ptr[i + 1];
237 for k in start..end {
238 let c = self.col_indices[k];
239 let dest = write_pos[c];
240 t_col_indices[dest] = i;
241 t_values[dest] = self.values[k];
242 write_pos[c] += 1;
243 }
244 }
245 HostCsr {
246 nrows: self.ncols,
247 ncols: self.nrows,
248 row_ptr: t_row_ptr,
249 col_indices: t_col_indices,
250 values: t_values,
251 }
252 }
253
254 pub fn matmul(&self, rhs: &HostCsr) -> SparseResult<HostCsr> {
266 if self.ncols != rhs.nrows {
267 return Err(SparseError::DimensionMismatch(format!(
268 "A.ncols ({}) != B.nrows ({})",
269 self.ncols, rhs.nrows
270 )));
271 }
272 let out_cols = rhs.ncols;
273 let mut c_row_ptr = vec![0usize; self.nrows + 1];
274 let mut c_col_indices: Vec<usize> = Vec::new();
275 let mut c_values: Vec<f64> = Vec::new();
276
277 let mut accum = vec![0.0f64; out_cols];
280 let mut touched = vec![false; out_cols];
281 let mut touched_cols: Vec<usize> = Vec::new();
282
283 for i in 0..self.nrows {
284 let a_start = self.row_ptr[i];
285 let a_end = self.row_ptr[i + 1];
286 for ak in a_start..a_end {
287 let a_col = self.col_indices[ak];
288 let a_val = self.values[ak];
289 let b_start = rhs.row_ptr[a_col];
290 let b_end = rhs.row_ptr[a_col + 1];
291 for bk in b_start..b_end {
292 let b_col = rhs.col_indices[bk];
293 if !touched[b_col] {
294 touched[b_col] = true;
295 touched_cols.push(b_col);
296 }
297 accum[b_col] += a_val * rhs.values[bk];
298 }
299 }
300 touched_cols.sort_unstable();
303 for &col in &touched_cols {
304 let v = accum[col];
305 if v != 0.0 {
306 c_col_indices.push(col);
307 c_values.push(v);
308 }
309 accum[col] = 0.0;
310 touched[col] = false;
311 }
312 touched_cols.clear();
313 c_row_ptr[i + 1] = c_col_indices.len();
314 }
315
316 Ok(HostCsr {
317 nrows: self.nrows,
318 ncols: out_cols,
319 row_ptr: c_row_ptr,
320 col_indices: c_col_indices,
321 values: c_values,
322 })
323 }
324
325 pub fn to_dense(&self) -> Vec<f64> {
329 let mut dense = vec![0.0f64; self.nrows * self.ncols];
330 for i in 0..self.nrows {
331 let start = self.row_ptr[i];
332 let end = self.row_ptr[i + 1];
333 for k in start..end {
334 dense[i * self.ncols + self.col_indices[k]] = self.values[k];
335 }
336 }
337 dense
338 }
339}
340
341pub fn dense_solve(a: &[f64], b: &[f64], n: usize) -> SparseResult<Vec<f64>> {
350 let mut m = a.to_vec();
351 let mut rhs = b.to_vec();
352
353 for col in 0..n {
354 let mut pivot_row = col;
356 let mut pivot_mag = m[col * n + col].abs();
357 for r in (col + 1)..n {
358 let mag = m[r * n + col].abs();
359 if mag > pivot_mag {
360 pivot_mag = mag;
361 pivot_row = r;
362 }
363 }
364 if pivot_mag < 1e-300 {
365 return Err(SparseError::SingularMatrix);
366 }
367 if pivot_row != col {
368 for c in 0..n {
369 m.swap(col * n + c, pivot_row * n + c);
370 }
371 rhs.swap(col, pivot_row);
372 }
373 let pivot = m[col * n + col];
374 for r in (col + 1)..n {
375 let factor = m[r * n + col] / pivot;
376 if factor != 0.0 {
377 for c in col..n {
378 m[r * n + c] -= factor * m[col * n + c];
379 }
380 rhs[r] -= factor * rhs[col];
381 }
382 }
383 }
384
385 let mut x = vec![0.0f64; n];
387 for col in (0..n).rev() {
388 let mut acc = rhs[col];
389 for c in (col + 1)..n {
390 acc -= m[col * n + c] * x[c];
391 }
392 x[col] = acc / m[col * n + col];
393 }
394 Ok(x)
395}
396
397#[inline]
400pub fn gpu_to_f64<T: GpuFloat>(v: T) -> f64 {
401 if T::SIZE == 4 {
402 f64::from(f32::from_bits(v.to_bits_u64() as u32))
403 } else {
404 f64::from_bits(v.to_bits_u64())
405 }
406}
407
408#[inline]
410pub fn f64_to_gpu<T: GpuFloat>(v: f64) -> T {
411 if T::SIZE == 4 {
412 T::from_bits_u64(u64::from((v as f32).to_bits()))
413 } else {
414 T::from_bits_u64(v.to_bits())
415 }
416}
417
418#[cfg(test)]
421pub(crate) fn laplacian_1d(n: usize) -> HostCsr {
422 let mut row_ptr = vec![0usize; n + 1];
423 let mut col_indices = Vec::new();
424 let mut values = Vec::new();
425 for i in 0..n {
426 if i > 0 {
427 col_indices.push(i - 1);
428 values.push(-1.0);
429 }
430 col_indices.push(i);
431 values.push(2.0);
432 if i + 1 < n {
433 col_indices.push(i + 1);
434 values.push(-1.0);
435 }
436 row_ptr[i + 1] = col_indices.len();
437 }
438 HostCsr {
439 nrows: n,
440 ncols: n,
441 row_ptr,
442 col_indices,
443 values,
444 }
445}
446
447#[cfg(test)]
450pub(crate) fn laplacian_2d(gx: usize, gy: usize) -> HostCsr {
451 let n = gx * gy;
452 let mut row_ptr = vec![0usize; n + 1];
453 let mut col_indices = Vec::new();
454 let mut values = Vec::new();
455 let idx = |x: usize, y: usize| -> usize { y * gx + x };
456 for y in 0..gy {
457 for x in 0..gx {
458 let mut entries: Vec<(usize, f64)> = Vec::new();
460 entries.push((idx(x, y), 4.0));
461 if x > 0 {
462 entries.push((idx(x - 1, y), -1.0));
463 }
464 if x + 1 < gx {
465 entries.push((idx(x + 1, y), -1.0));
466 }
467 if y > 0 {
468 entries.push((idx(x, y - 1), -1.0));
469 }
470 if y + 1 < gy {
471 entries.push((idx(x, y + 1), -1.0));
472 }
473 entries.sort_by_key(|&(c, _)| c);
474 for (c, v) in entries {
475 col_indices.push(c);
476 values.push(v);
477 }
478 row_ptr[idx(x, y) + 1] = col_indices.len();
479 }
480 }
481 HostCsr {
482 nrows: n,
483 ncols: n,
484 row_ptr,
485 col_indices,
486 values,
487 }
488}
489
490#[cfg(test)]
491mod tests {
492 use super::*;
493
494 #[test]
495 fn new_validates_row_ptr_length() {
496 let r = HostCsr::new(2, 2, vec![0, 1], vec![0], vec![1.0]);
497 assert!(r.is_err());
498 }
499
500 #[test]
501 fn new_validates_col_range() {
502 let r = HostCsr::new(2, 2, vec![0, 1, 2], vec![0, 5], vec![1.0, 2.0]);
503 assert!(r.is_err());
504 }
505
506 #[test]
507 fn diagonal_extraction() {
508 let a = laplacian_1d(4);
509 assert_eq!(a.diagonal(), vec![2.0, 2.0, 2.0, 2.0]);
510 }
511
512 #[test]
513 fn get_returns_entries() {
514 let a = laplacian_1d(3);
515 assert_eq!(a.get(0, 0), Some(2.0));
516 assert_eq!(a.get(0, 1), Some(-1.0));
517 assert_eq!(a.get(0, 2), None);
518 assert_eq!(a.get(1, 0), Some(-1.0));
519 }
520
521 #[test]
522 fn matvec_laplacian() {
523 let a = laplacian_1d(4);
524 let y = a.matvec(&[1.0, 1.0, 1.0, 1.0]);
526 assert_eq!(y, vec![1.0, 0.0, 0.0, 1.0]);
527 }
528
529 #[test]
530 fn transpose_of_symmetric_is_self() {
531 let a = laplacian_1d(5);
532 let at = a.transpose();
533 assert_eq!(at.nrows, a.nrows);
534 assert_eq!(at.ncols, a.ncols);
535 for i in 0..a.nrows {
537 for j in 0..a.ncols {
538 assert_eq!(a.get(i, j), at.get(i, j));
539 }
540 }
541 }
542
543 #[test]
544 fn transpose_rectangular() {
545 let a = HostCsr::new(2, 3, vec![0, 2, 3], vec![0, 2, 1], vec![1.0, 2.0, 3.0])
549 .expect("valid csr");
550 let at = a.transpose();
551 assert_eq!(at.nrows, 3);
552 assert_eq!(at.ncols, 2);
553 assert_eq!(at.get(0, 0), Some(1.0));
554 assert_eq!(at.get(2, 0), Some(2.0));
555 assert_eq!(at.get(1, 1), Some(3.0));
556 }
557
558 #[test]
559 fn matmul_identity() {
560 let a = laplacian_1d(4);
562 let eye = HostCsr::new(
563 4,
564 4,
565 vec![0, 1, 2, 3, 4],
566 vec![0, 1, 2, 3],
567 vec![1.0, 1.0, 1.0, 1.0],
568 )
569 .expect("valid csr");
570 let c = eye.matmul(&a).expect("matmul");
571 for i in 0..4 {
572 for j in 0..4 {
573 assert_eq!(c.get(i, j), a.get(i, j));
574 }
575 }
576 }
577
578 #[test]
579 fn matmul_matches_dense() {
580 let a = HostCsr::new(
582 3,
583 3,
584 vec![0, 2, 4, 6],
585 vec![0, 1, 1, 2, 0, 2],
586 vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0],
587 )
588 .expect("valid");
589 let b = HostCsr::new(3, 2, vec![0, 1, 2, 3], vec![0, 1, 0], vec![7.0, 8.0, 9.0])
590 .expect("valid");
591 let c = a.matmul(&b).expect("matmul");
592 assert_eq!(c.get(0, 0), Some(7.0));
594 assert_eq!(c.get(0, 1), Some(16.0));
595 assert_eq!(c.get(1, 0), Some(36.0));
596 assert_eq!(c.get(1, 1), Some(24.0));
597 assert_eq!(c.get(2, 0), Some(89.0));
598 assert_eq!(c.get(2, 1), None);
599 }
600
601 #[test]
602 fn dense_solve_small() {
603 let a = vec![2.0, 1.0, 1.0, 3.0];
605 let b = vec![3.0, 5.0];
606 let x = dense_solve(&a, &b, 2).expect("solve");
607 assert!((x[0] - 0.8).abs() < 1e-12);
608 assert!((x[1] - 1.4).abs() < 1e-12);
609 }
610
611 #[test]
612 fn dense_solve_singular_errors() {
613 let a = vec![1.0, 2.0, 2.0, 4.0];
614 let b = vec![1.0, 2.0];
615 assert!(dense_solve(&a, &b, 2).is_err());
616 }
617
618 #[test]
619 fn gpu_f64_roundtrip() {
620 let v = 3.5_f64;
621 let g = f64_to_gpu::<f64>(v);
622 assert!((gpu_to_f64(g) - v).abs() < 1e-15);
623 let gf = f64_to_gpu::<f32>(v);
624 assert!((gpu_to_f64(gf) - v).abs() < 1e-6);
625 }
626
627 #[test]
628 fn laplacian_2d_structure() {
629 let a = laplacian_2d(3, 3);
630 assert_eq!(a.nrows, 9);
631 assert_eq!(a.get(4, 4), Some(4.0));
633 let start = a.row_ptr[4];
634 let end = a.row_ptr[4 + 1];
635 assert_eq!(end - start, 5);
636 }
637}