1#[derive(Debug, Clone)]
17pub struct SparseMatrixGpu {
18 pub n: usize,
20 pub nnz: usize,
22 pub row_ptr: Vec<usize>,
24 pub col_idx: Vec<usize>,
26 pub values: Vec<f64>,
28}
29
30impl SparseMatrixGpu {
31 pub fn new(n: usize) -> Self {
33 Self {
34 n,
35 nnz: 0,
36 row_ptr: vec![0usize; n + 1],
37 col_idx: Vec::new(),
38 values: Vec::new(),
39 }
40 }
41
42 pub fn from_csr(n: usize, row_ptr: Vec<usize>, col_idx: Vec<usize>, values: Vec<f64>) -> Self {
47 debug_assert_eq!(row_ptr.len(), n + 1);
48 let nnz = values.len();
49 Self {
50 n,
51 nnz,
52 row_ptr,
53 col_idx,
54 values,
55 }
56 }
57
58 pub fn add_entry(&mut self, row: usize, col: usize, val: f64) {
62 self.col_idx.push(col);
64 self.values.push(val);
65 if row + 1 < self.row_ptr.len() {
67 self.row_ptr[row + 1] += 1;
68 }
69 self.nnz += 1;
70 }
71
72 pub fn finalize(&mut self) {
77 for i in 1..=self.n {
79 self.row_ptr[i] += self.row_ptr[i - 1];
80 }
81 self.nnz = *self.row_ptr.last().unwrap_or(&0);
82 }
83
84 pub fn nnz(&self) -> usize {
86 self.nnz
87 }
88
89 pub fn rows(&self) -> usize {
91 self.n
92 }
93
94 pub fn diagonal(&self) -> Vec<f64> {
98 let mut diag = vec![0.0f64; self.n];
99 for (row, d) in diag.iter_mut().enumerate() {
100 let start = self.row_ptr[row];
101 let end = self.row_ptr[row + 1];
102 for k in start..end {
103 if self.col_idx[k] == row {
104 *d = self.values[k];
105 }
106 }
107 }
108 diag
109 }
110
111 pub fn is_symmetric(&self) -> bool {
113 for row in 0..self.n {
114 let start = self.row_ptr[row];
115 let end = self.row_ptr[row + 1];
116 for k in start..end {
117 let col = self.col_idx[k];
118 let val = self.values[k];
119 let mut found = false;
121 let cs = self.row_ptr[col];
122 let ce = self.row_ptr[col + 1];
123 for m in cs..ce {
124 if self.col_idx[m] == row {
125 if (self.values[m] - val).abs() > 1e-12 {
126 return false;
127 }
128 found = true;
129 break;
130 }
131 }
132 if !found {
133 return false;
134 }
135 }
136 }
137 true
138 }
139
140 pub fn frobenius_norm(&self) -> f64 {
142 self.values.iter().map(|v| v * v).sum::<f64>().sqrt()
143 }
144}
145
146pub fn sparse_identity(n: usize) -> SparseMatrixGpu {
150 let row_ptr: Vec<usize> = (0..=n).collect();
151 let col_idx: Vec<usize> = (0..n).collect();
152 let values = vec![1.0f64; n];
153 SparseMatrixGpu::from_csr(n, row_ptr, col_idx, values)
154}
155
156pub fn sparse_diagonal_matrix(diag: &[f64]) -> SparseMatrixGpu {
158 let n = diag.len();
159 let row_ptr: Vec<usize> = (0..=n).collect();
160 let col_idx: Vec<usize> = (0..n).collect();
161 let values = diag.to_vec();
162 SparseMatrixGpu::from_csr(n, row_ptr, col_idx, values)
163}
164
165pub fn gpu_spmv(mat: &SparseMatrixGpu, x: &[f64]) -> Vec<f64> {
172 assert_eq!(x.len(), mat.n, "gpu_spmv: x length mismatch");
173 let mut y = vec![0.0f64; mat.n];
174 for (row, y_row) in y.iter_mut().enumerate() {
175 let start = mat.row_ptr[row];
176 let end = mat.row_ptr[row + 1];
177 let mut sum = 0.0f64;
178 for k in start..end {
179 sum += mat.values[k] * x[mat.col_idx[k]];
180 }
181 *y_row = sum;
182 }
183 y
184}
185
186pub fn gpu_dot(a: &[f64], b: &[f64]) -> f64 {
190 a.iter().zip(b.iter()).map(|(ai, bi)| ai * bi).sum()
191}
192
193pub fn gpu_axpy(a: f64, x: &[f64], y: &mut [f64]) {
198 assert_eq!(x.len(), y.len(), "gpu_axpy: length mismatch");
199 for (yi, &xi) in y.iter_mut().zip(x.iter()) {
200 *yi += a * xi;
201 }
202}
203
204pub fn gpu_cg_solver(
212 mat: &SparseMatrixGpu,
213 b: &[f64],
214 max_iter: usize,
215 tol: f64,
216) -> (Vec<f64>, usize, f64) {
217 let n = mat.n;
218 let mut x = vec![0.0f64; n];
219 let mut r = b.to_vec(); let mut p = r.clone();
221 let mut rr = gpu_dot(&r, &r);
222 let b_norm = rr.sqrt().max(1e-100);
223
224 for iter in 0..max_iter {
225 if rr.sqrt() / b_norm < tol {
226 return (x, iter, rr.sqrt());
227 }
228 let ap = gpu_spmv(mat, &p);
229 let pap = gpu_dot(&p, &ap);
230 if pap.abs() < 1e-300 {
231 break;
232 }
233 let alpha = rr / pap;
234 gpu_axpy(alpha, &p, &mut x);
235 gpu_axpy(-alpha, &ap, &mut r);
236 let rr_new = gpu_dot(&r, &r);
237 let beta = rr_new / rr.max(1e-300);
238 for i in 0..n {
240 p[i] = r[i] + beta * p[i];
241 }
242 rr = rr_new;
243 }
244 (x, max_iter, rr.sqrt())
245}
246
247pub fn gpu_jacobi_preconditioner(mat: &SparseMatrixGpu) -> Vec<f64> {
252 mat.diagonal()
253 .iter()
254 .map(|&d| if d.abs() > 1e-15 { 1.0 / d } else { 1.0 })
255 .collect()
256}
257
258pub fn gpu_pcg_solver(
263 mat: &SparseMatrixGpu,
264 b: &[f64],
265 precond: &[f64],
266 max_iter: usize,
267 tol: f64,
268) -> (Vec<f64>, usize, f64) {
269 let n = mat.n;
270 let mut x = vec![0.0f64; n];
271 let mut r = b.to_vec();
272 let mut z: Vec<f64> = r
274 .iter()
275 .zip(precond.iter())
276 .map(|(ri, mi)| ri * mi)
277 .collect();
278 let mut p = z.clone();
279 let mut rz = gpu_dot(&r, &z);
280 let b_norm = gpu_dot(b, b).sqrt().max(1e-100);
281
282 for iter in 0..max_iter {
283 if gpu_dot(&r, &r).sqrt() / b_norm < tol {
284 return (x, iter, gpu_dot(&r, &r).sqrt());
285 }
286 let ap = gpu_spmv(mat, &p);
287 let pap = gpu_dot(&p, &ap);
288 if pap.abs() < 1e-300 {
289 break;
290 }
291 let alpha = rz / pap;
292 gpu_axpy(alpha, &p, &mut x);
293 gpu_axpy(-alpha, &ap, &mut r);
294 z = r
295 .iter()
296 .zip(precond.iter())
297 .map(|(ri, mi)| ri * mi)
298 .collect();
299 let rz_new = gpu_dot(&r, &z);
300 let beta = rz_new / rz.max(1e-300);
301 for i in 0..n {
302 p[i] = z[i] + beta * p[i];
303 }
304 rz = rz_new;
305 }
306 (x, max_iter, gpu_dot(&r, &r).sqrt())
307}
308
309#[derive(Debug, Clone)]
313pub struct GpuSparseSolverStats {
314 pub iterations: usize,
316 pub final_residual: f64,
318 pub converged: bool,
320 pub time_ms: f64,
322}
323
324impl GpuSparseSolverStats {
325 pub fn new(iterations: usize, final_residual: f64, converged: bool, time_ms: f64) -> Self {
327 Self {
328 iterations,
329 final_residual,
330 converged,
331 time_ms,
332 }
333 }
334}
335
336#[cfg(test)]
340mod tests {
341 use super::*;
342
343 fn build_3x3_spd() -> SparseMatrixGpu {
350 let row_ptr = vec![0, 2, 5, 7];
351 let col_idx = vec![0, 1, 0, 1, 2, 1, 2];
352 let values = vec![4.0, -1.0, -1.0, 4.0, -1.0, -1.0, 4.0];
353 SparseMatrixGpu::from_csr(3, row_ptr, col_idx, values)
354 }
355
356 #[test]
359 fn test_identity_spmv_returns_input() {
360 let id = sparse_identity(4);
361 let x = vec![1.0, 2.0, 3.0, 4.0];
362 let y = gpu_spmv(&id, &x);
363 for (yi, xi) in y.iter().zip(x.iter()) {
364 assert!((yi - xi).abs() < 1e-12);
365 }
366 }
367
368 #[test]
369 fn test_identity_nnz() {
370 let id = sparse_identity(5);
371 assert_eq!(id.nnz(), 5);
372 }
373
374 #[test]
375 fn test_identity_rows() {
376 let id = sparse_identity(7);
377 assert_eq!(id.rows(), 7);
378 }
379
380 #[test]
381 fn test_identity_diagonal() {
382 let id = sparse_identity(4);
383 let diag = id.diagonal();
384 assert_eq!(diag, vec![1.0; 4]);
385 }
386
387 #[test]
388 fn test_identity_frobenius() {
389 let n = 9usize;
390 let id = sparse_identity(n);
391 let expected = (n as f64).sqrt();
392 assert!((id.frobenius_norm() - expected).abs() < 1e-10);
393 }
394
395 #[test]
396 fn test_identity_is_symmetric() {
397 assert!(sparse_identity(4).is_symmetric());
398 }
399
400 #[test]
403 fn test_diagonal_matrix_spmv() {
404 let d = vec![2.0, 3.0, 5.0];
405 let mat = sparse_diagonal_matrix(&d);
406 let x = vec![1.0, 1.0, 1.0];
407 let y = gpu_spmv(&mat, &x);
408 assert!((y[0] - 2.0).abs() < 1e-12);
409 assert!((y[1] - 3.0).abs() < 1e-12);
410 assert!((y[2] - 5.0).abs() < 1e-12);
411 }
412
413 #[test]
414 fn test_diagonal_matrix_frobenius() {
415 let d = vec![3.0, 4.0];
416 let mat = sparse_diagonal_matrix(&d);
417 assert!((mat.frobenius_norm() - 5.0).abs() < 1e-10);
418 }
419
420 #[test]
421 fn test_diagonal_matrix_is_symmetric() {
422 let mat = sparse_diagonal_matrix(&[1.0, 2.0, 3.0]);
423 assert!(mat.is_symmetric());
424 }
425
426 #[test]
427 fn test_sparse_identity_zero_size() {
428 let id = sparse_identity(0);
429 assert_eq!(id.nnz(), 0);
430 assert_eq!(id.rows(), 0);
431 }
432
433 #[test]
436 fn test_gpu_dot_basic() {
437 assert!((gpu_dot(&[1.0, 2.0, 3.0], &[4.0, 5.0, 6.0]) - 32.0).abs() < 1e-12);
438 }
439
440 #[test]
441 fn test_gpu_dot_empty() {
442 assert!((gpu_dot(&[], &[])).abs() < 1e-15);
443 }
444
445 #[test]
446 fn test_gpu_dot_orthogonal() {
447 assert!((gpu_dot(&[1.0, 0.0], &[0.0, 1.0])).abs() < 1e-15);
448 }
449
450 #[test]
453 fn test_gpu_axpy_basic() {
454 let x = vec![1.0, 2.0, 3.0];
455 let mut y = vec![4.0, 5.0, 6.0];
456 gpu_axpy(2.0, &x, &mut y);
457 assert!((y[0] - 6.0).abs() < 1e-12);
458 assert!((y[1] - 9.0).abs() < 1e-12);
459 assert!((y[2] - 12.0).abs() < 1e-12);
460 }
461
462 #[test]
463 fn test_gpu_axpy_zero_alpha() {
464 let x = vec![1.0, 2.0];
465 let mut y = vec![3.0, 4.0];
466 gpu_axpy(0.0, &x, &mut y);
467 assert!((y[0] - 3.0).abs() < 1e-12);
468 assert!((y[1] - 4.0).abs() < 1e-12);
469 }
470
471 #[test]
474 fn test_spmv_3x3_spd() {
475 let mat = build_3x3_spd();
476 let x = vec![1.0, 0.0, 0.0];
477 let y = gpu_spmv(&mat, &x);
478 assert!((y[0] - 4.0).abs() < 1e-12);
479 assert!((y[1] + 1.0).abs() < 1e-12);
480 assert!((y[2]).abs() < 1e-12);
481 }
482
483 #[test]
484 fn test_spmv_zeros_input() {
485 let mat = build_3x3_spd();
486 let y = gpu_spmv(&mat, &[0.0, 0.0, 0.0]);
487 for yi in y {
488 assert!(yi.abs() < 1e-15);
489 }
490 }
491
492 #[test]
495 fn test_from_csr_nnz() {
496 let mat = build_3x3_spd();
497 assert_eq!(mat.nnz(), 7);
498 }
499
500 #[test]
501 fn test_add_entry_finalize() {
502 let mut mat = SparseMatrixGpu::new(2);
503 mat.add_entry(0, 0, 2.0);
504 mat.add_entry(0, 1, -1.0);
505 mat.add_entry(1, 0, -1.0);
506 mat.add_entry(1, 1, 2.0);
507 mat.finalize();
508 assert_eq!(mat.nnz(), 4);
509 let y = gpu_spmv(&mat, &[1.0, 0.0]);
510 assert!((y[0] - 2.0).abs() < 1e-12);
511 assert!((y[1] + 1.0).abs() < 1e-12);
512 }
513
514 #[test]
517 fn test_cg_converges_3x3() {
518 let mat = build_3x3_spd();
519 let b = vec![1.0, 0.0, 0.0];
520 let (x, iters, res) = gpu_cg_solver(&mat, &b, 100, 1e-10);
521 assert!(iters < 100, "CG did not converge: iters={iters}");
522 let ax = gpu_spmv(&mat, &x);
524 for (ai, bi) in ax.iter().zip(b.iter()) {
525 assert!((ai - bi).abs() < 1e-8, "residual too large");
526 }
527 let _ = res; }
529
530 #[test]
531 fn test_cg_identity_system() {
532 let id = sparse_identity(3);
533 let b = vec![1.0, 2.0, 3.0];
534 let (x, _iters, _res) = gpu_cg_solver(&id, &b, 50, 1e-10);
535 for (xi, bi) in x.iter().zip(b.iter()) {
536 assert!((xi - bi).abs() < 1e-8);
537 }
538 }
539
540 #[test]
543 fn test_jacobi_preconditioner_diagonal() {
544 let d = vec![2.0, 4.0, 5.0];
545 let mat = sparse_diagonal_matrix(&d);
546 let prec = gpu_jacobi_preconditioner(&mat);
547 assert!((prec[0] - 0.5).abs() < 1e-12);
548 assert!((prec[1] - 0.25).abs() < 1e-12);
549 assert!((prec[2] - 0.2).abs() < 1e-12);
550 }
551
552 #[test]
553 fn test_jacobi_preconditioner_identity() {
554 let id = sparse_identity(3);
555 let prec = gpu_jacobi_preconditioner(&id);
556 for p in prec {
557 assert!((p - 1.0).abs() < 1e-12);
558 }
559 }
560
561 #[test]
564 fn test_pcg_converges_3x3() {
565 let mat = build_3x3_spd();
566 let b = vec![1.0, 0.0, 1.0];
567 let prec = gpu_jacobi_preconditioner(&mat);
568 let (x, iters, _res) = gpu_pcg_solver(&mat, &b, &prec, 100, 1e-10);
569 assert!(iters < 100);
570 let ax = gpu_spmv(&mat, &x);
571 for (ai, bi) in ax.iter().zip(b.iter()) {
572 assert!((ai - bi).abs() < 1e-7);
573 }
574 }
575
576 #[test]
577 fn test_pcg_identity_trivial() {
578 let id = sparse_identity(4);
579 let b = vec![1.0, 2.0, 3.0, 4.0];
580 let prec = gpu_jacobi_preconditioner(&id);
581 let (x, _iters, _res) = gpu_pcg_solver(&id, &b, &prec, 10, 1e-12);
582 for (xi, bi) in x.iter().zip(b.iter()) {
583 assert!((xi - bi).abs() < 1e-8);
584 }
585 }
586
587 #[test]
590 fn test_solver_stats_fields() {
591 let s = GpuSparseSolverStats::new(5, 1e-8, true, 0.0);
592 assert_eq!(s.iterations, 5);
593 assert!(s.converged);
594 assert!((s.final_residual - 1e-8).abs() < 1e-20);
595 }
596
597 #[test]
600 fn test_asymmetric_matrix() {
601 let row_ptr = vec![0, 2, 3, 3];
603 let col_idx = vec![0, 1, 1, 2];
604 let values = vec![1.0, 2.0, 3.0, 4.0];
605 let mat = SparseMatrixGpu::from_csr(3, row_ptr, col_idx, values);
606 assert!(!mat.is_symmetric());
607 }
608
609 #[test]
610 fn test_frobenius_zero_matrix() {
611 let mat = SparseMatrixGpu::new(4);
612 assert!((mat.frobenius_norm()).abs() < 1e-15);
613 }
614
615 #[test]
618 fn test_new_empty_matrix() {
619 let mat = SparseMatrixGpu::new(3);
620 assert_eq!(mat.n, 3);
621 assert_eq!(mat.nnz, 0);
622 assert_eq!(mat.row_ptr, vec![0; 4]);
623 }
624
625 #[test]
626 fn test_diagonal_of_empty_matrix() {
627 let mat = SparseMatrixGpu::new(3);
628 let diag = mat.diagonal();
629 assert_eq!(diag, vec![0.0; 3]);
630 }
631
632 #[test]
633 fn test_cg_zero_rhs() {
634 let id = sparse_identity(3);
635 let b = vec![0.0; 3];
636 let (x, _iters, res) = gpu_cg_solver(&id, &b, 20, 1e-12);
637 for xi in &x {
638 assert!(xi.abs() < 1e-12);
639 }
640 assert!(res < 1e-10);
641 }
642}