1use crate::error::{SparseError, SparseResult};
9use crate::learned_smoother::types::{LearnedSmootherConfig, Smoother, SmootherMetrics};
10
11type CoarseningResult = (
14 Vec<f64>,
15 Vec<usize>,
16 Vec<usize>,
17 Vec<f64>,
18 Vec<usize>,
19 Vec<usize>,
20 usize,
21);
22
23fn csr_matvec(a_values: &[f64], a_row_ptr: &[usize], a_col_idx: &[usize], x: &[f64]) -> Vec<f64> {
29 let n = a_row_ptr.len().saturating_sub(1);
30 let mut y = vec![0.0; n];
31 for i in 0..n {
32 let start = a_row_ptr[i];
33 let end = a_row_ptr[i + 1];
34 let mut sum = 0.0;
35 for pos in start..end {
36 sum += a_values[pos] * x[a_col_idx[pos]];
37 }
38 y[i] = sum;
39 }
40 y
41}
42
43fn compute_residual(
45 a_values: &[f64],
46 a_row_ptr: &[usize],
47 a_col_idx: &[usize],
48 x: &[f64],
49 b: &[f64],
50) -> Vec<f64> {
51 let ax = csr_matvec(a_values, a_row_ptr, a_col_idx, x);
52 b.iter()
53 .zip(ax.iter())
54 .map(|(&bi, &axi)| bi - axi)
55 .collect()
56}
57
58fn vec_norm(v: &[f64]) -> f64 {
60 v.iter().map(|x| x * x).sum::<f64>().sqrt()
61}
62
63fn extract_diagonal(
65 a_values: &[f64],
66 a_row_ptr: &[usize],
67 a_col_idx: &[usize],
68 n: usize,
69) -> Vec<f64> {
70 let mut diag = vec![0.0; n];
71 for i in 0..n {
72 let start = a_row_ptr[i];
73 let end = a_row_ptr[i + 1];
74 for pos in start..end {
75 if a_col_idx[pos] == i {
76 diag[i] = a_values[pos];
77 break;
78 }
79 }
80 }
81 diag
82}
83
84fn jacobi_smooth(
86 a_values: &[f64],
87 a_row_ptr: &[usize],
88 a_col_idx: &[usize],
89 x: &mut [f64],
90 b: &[f64],
91 diag_inv: &[f64],
92 omega: f64,
93 n_sweeps: usize,
94) {
95 let n = x.len();
96 for _ in 0..n_sweeps {
97 let r = compute_residual(a_values, a_row_ptr, a_col_idx, x, b);
98 for i in 0..n {
99 x[i] += omega * diag_inv[i] * r[i];
100 }
101 }
102}
103
104fn build_coarsening(n: usize) -> CoarseningResult {
114 let n_c = (n + 1) / 2;
115
116 let mut r_values = Vec::new();
118 let mut r_col_idx = Vec::new();
119 let mut r_row_ptr = vec![0usize];
120
121 for c in 0..n_c {
122 let fine = 2 * c;
123 if fine > 0 {
124 r_values.push(0.25);
125 r_col_idx.push(fine - 1);
126 }
127 r_values.push(0.5);
128 r_col_idx.push(fine);
129 if fine + 1 < n {
130 r_values.push(0.25);
131 r_col_idx.push(fine + 1);
132 }
133 r_row_ptr.push(r_values.len());
134 }
135
136 let mut p_values = Vec::new();
138 let mut p_col_idx = Vec::new();
139 let mut p_row_ptr = vec![0usize];
140
141 for i in 0..n {
142 if i % 2 == 0 {
143 let c = i / 2;
145 p_values.push(1.0);
146 p_col_idx.push(c);
147 } else {
148 let c_left = i / 2;
150 let c_right = c_left + 1;
151 p_values.push(0.5);
152 p_col_idx.push(c_left);
153 if c_right < n_c {
154 p_values.push(0.5);
155 p_col_idx.push(c_right);
156 }
157 }
158 p_row_ptr.push(p_values.len());
159 }
160
161 (
162 r_values, r_row_ptr, r_col_idx, p_values, p_row_ptr, p_col_idx, n_c,
163 )
164}
165
166fn compute_coarse_operator(
170 a_values: &[f64],
171 a_row_ptr: &[usize],
172 a_col_idx: &[usize],
173 n: usize,
174 r_values: &[f64],
175 r_row_ptr: &[usize],
176 r_col_idx: &[usize],
177 p_values: &[f64],
178 p_row_ptr: &[usize],
179 p_col_idx: &[usize],
180 n_c: usize,
181) -> (Vec<f64>, Vec<usize>, Vec<usize>) {
182 let mut ac_dense = vec![vec![0.0; n_c]; n_c];
184
185 for ic in 0..n_c {
186 let mut e_c = vec![0.0; n_c];
188 e_c[ic] = 1.0;
189
190 let p_e = csr_matvec(p_values, p_row_ptr, p_col_idx, &e_c);
192
193 let a_p_e = csr_matvec(a_values, a_row_ptr, a_col_idx, &p_e);
195
196 let r_a_p_e = csr_matvec(r_values, r_row_ptr, r_col_idx, &a_p_e);
198
199 for jc in 0..n_c {
200 ac_dense[jc][ic] = r_a_p_e[jc];
201 }
202 }
203
204 let mut ac_values = Vec::new();
206 let mut ac_col_idx = Vec::new();
207 let mut ac_row_ptr = vec![0usize];
208
209 for i in 0..n_c {
210 for j in 0..n_c {
211 if ac_dense[i][j].abs() > 1e-14 {
212 ac_values.push(ac_dense[i][j]);
213 ac_col_idx.push(j);
214 }
215 }
216 ac_row_ptr.push(ac_values.len());
217 }
218
219 (ac_values, ac_row_ptr, ac_col_idx)
220}
221
222fn direct_solve(
224 a_values: &[f64],
225 a_row_ptr: &[usize],
226 a_col_idx: &[usize],
227 b: &[f64],
228 n: usize,
229) -> SparseResult<Vec<f64>> {
230 if n == 0 {
231 return Ok(Vec::new());
232 }
233
234 let mut dense = vec![vec![0.0; n]; n];
236 for i in 0..n {
237 let start = a_row_ptr[i];
238 let end = a_row_ptr[i + 1];
239 for pos in start..end {
240 dense[i][a_col_idx[pos]] = a_values[pos];
241 }
242 }
243
244 let mut aug: Vec<Vec<f64>> = dense
246 .iter()
247 .enumerate()
248 .map(|(i, row)| {
249 let mut r = row.clone();
250 r.push(b[i]);
251 r
252 })
253 .collect();
254
255 for col in 0..n {
257 let mut max_val = aug[col][col].abs();
259 let mut max_row = col;
260 for row in (col + 1)..n {
261 if aug[row][col].abs() > max_val {
262 max_val = aug[row][col].abs();
263 max_row = row;
264 }
265 }
266 if max_val < 1e-14 {
267 return Err(SparseError::SingularMatrix(
268 "Coarse grid operator is singular".to_string(),
269 ));
270 }
271 aug.swap(col, max_row);
272
273 let pivot = aug[col][col];
274 for row in (col + 1)..n {
275 let factor = aug[row][col] / pivot;
276 for j in col..=n {
277 let val = aug[col][j];
278 aug[row][j] -= factor * val;
279 }
280 }
281 }
282
283 let mut x = vec![0.0; n];
285 for i in (0..n).rev() {
286 let mut sum = aug[i][n];
287 for j in (i + 1)..n {
288 sum -= aug[i][j] * x[j];
289 }
290 x[i] = sum / aug[i][i];
291 }
292
293 Ok(x)
294}
295
296pub struct HybridMultigridSolver {
312 a_values: Vec<f64>,
314 a_row_ptr: Vec<usize>,
315 a_col_idx: Vec<usize>,
316 n: usize,
317
318 ac_values: Vec<f64>,
320 ac_row_ptr: Vec<usize>,
321 ac_col_idx: Vec<usize>,
322 n_c: usize,
323
324 r_values: Vec<f64>,
326 r_row_ptr: Vec<usize>,
327 r_col_idx: Vec<usize>,
328 p_values: Vec<f64>,
329 p_row_ptr: Vec<usize>,
330 p_col_idx: Vec<usize>,
331
332 smoother: Box<dyn Smoother>,
334
335 diag_inv: Vec<f64>,
337
338 config: LearnedSmootherConfig,
340}
341
342impl HybridMultigridSolver {
343 pub fn new(
350 a_values: Vec<f64>,
351 a_row_ptr: Vec<usize>,
352 a_col_idx: Vec<usize>,
353 smoother: Box<dyn Smoother>,
354 config: LearnedSmootherConfig,
355 ) -> SparseResult<Self> {
356 let n = a_row_ptr.len().saturating_sub(1);
357 if n == 0 {
358 return Err(SparseError::ValueError(
359 "Matrix dimension must be positive".to_string(),
360 ));
361 }
362
363 let (r_values, r_row_ptr, r_col_idx, p_values, p_row_ptr, p_col_idx, n_c) =
365 build_coarsening(n);
366
367 let (ac_values, ac_row_ptr, ac_col_idx) = compute_coarse_operator(
369 &a_values, &a_row_ptr, &a_col_idx, n, &r_values, &r_row_ptr, &r_col_idx, &p_values,
370 &p_row_ptr, &p_col_idx, n_c,
371 );
372
373 let diag = extract_diagonal(&a_values, &a_row_ptr, &a_col_idx, n);
375 let diag_inv: Vec<f64> = diag
376 .iter()
377 .map(|&d| if d.abs() > f64::EPSILON { 1.0 / d } else { 1.0 })
378 .collect();
379
380 Ok(Self {
381 a_values,
382 a_row_ptr,
383 a_col_idx,
384 n,
385 ac_values,
386 ac_row_ptr,
387 ac_col_idx,
388 n_c,
389 r_values,
390 r_row_ptr,
391 r_col_idx,
392 p_values,
393 p_row_ptr,
394 p_col_idx,
395 smoother,
396 diag_inv,
397 config,
398 })
399 }
400
401 fn v_cycle(&self, x: &mut [f64], b: &[f64]) -> SparseResult<()> {
403 self.smoother.smooth(
405 &self.a_values,
406 &self.a_row_ptr,
407 &self.a_col_idx,
408 x,
409 b,
410 self.config.pre_sweeps,
411 )?;
412
413 let r_fine = compute_residual(&self.a_values, &self.a_row_ptr, &self.a_col_idx, x, b);
415
416 let r_coarse = csr_matvec(&self.r_values, &self.r_row_ptr, &self.r_col_idx, &r_fine);
418
419 let e_coarse = direct_solve(
421 &self.ac_values,
422 &self.ac_row_ptr,
423 &self.ac_col_idx,
424 &r_coarse,
425 self.n_c,
426 )?;
427
428 let e_fine = csr_matvec(&self.p_values, &self.p_row_ptr, &self.p_col_idx, &e_coarse);
430
431 for i in 0..self.n {
433 x[i] += e_fine[i];
434 }
435
436 self.smoother.smooth(
438 &self.a_values,
439 &self.a_row_ptr,
440 &self.a_col_idx,
441 x,
442 b,
443 self.config.post_sweeps,
444 )?;
445
446 Ok(())
447 }
448
449 fn v_cycle_jacobi(&self, x: &mut [f64], b: &[f64]) -> SparseResult<()> {
451 let omega = self.config.omega;
452 let pre = self.config.pre_sweeps;
453 let post = self.config.post_sweeps;
454
455 jacobi_smooth(
457 &self.a_values,
458 &self.a_row_ptr,
459 &self.a_col_idx,
460 x,
461 b,
462 &self.diag_inv,
463 omega,
464 pre,
465 );
466
467 let r_fine = compute_residual(&self.a_values, &self.a_row_ptr, &self.a_col_idx, x, b);
469 let r_coarse = csr_matvec(&self.r_values, &self.r_row_ptr, &self.r_col_idx, &r_fine);
470
471 let e_coarse = direct_solve(
473 &self.ac_values,
474 &self.ac_row_ptr,
475 &self.ac_col_idx,
476 &r_coarse,
477 self.n_c,
478 )?;
479
480 let e_fine = csr_matvec(&self.p_values, &self.p_row_ptr, &self.p_col_idx, &e_coarse);
482 for i in 0..self.n {
483 x[i] += e_fine[i];
484 }
485
486 jacobi_smooth(
488 &self.a_values,
489 &self.a_row_ptr,
490 &self.a_col_idx,
491 x,
492 b,
493 &self.diag_inv,
494 omega,
495 post,
496 );
497
498 Ok(())
499 }
500
501 pub fn solve(&self, b: &[f64]) -> SparseResult<Vec<f64>> {
506 if b.len() != self.n {
507 return Err(SparseError::DimensionMismatch {
508 expected: self.n,
509 found: b.len(),
510 });
511 }
512
513 let mut x = vec![0.0; self.n];
514 let tol = self.config.convergence_tol;
515 let max_iter = self.config.max_training_steps;
516
517 let r0 = compute_residual(&self.a_values, &self.a_row_ptr, &self.a_col_idx, &x, b);
518 let norm0 = vec_norm(&r0);
519 if norm0 < tol {
520 return Ok(x);
521 }
522
523 let mut use_learned = true;
524 let mut prev_norm = norm0;
525
526 for _iter in 0..max_iter {
527 if use_learned {
528 let x_backup: Vec<f64> = x.clone();
529 let result = self.v_cycle(&mut x, b);
530
531 let r = compute_residual(&self.a_values, &self.a_row_ptr, &self.a_col_idx, &x, b);
532 let norm_r = vec_norm(&r);
533
534 if result.is_err() || norm_r > prev_norm * 2.0 {
536 x.copy_from_slice(&x_backup);
537 use_learned = false;
538 self.v_cycle_jacobi(&mut x, b)?;
539 let r2 =
540 compute_residual(&self.a_values, &self.a_row_ptr, &self.a_col_idx, &x, b);
541 prev_norm = vec_norm(&r2);
542 } else {
543 prev_norm = norm_r;
544 }
545 } else {
546 self.v_cycle_jacobi(&mut x, b)?;
547 let r = compute_residual(&self.a_values, &self.a_row_ptr, &self.a_col_idx, &x, b);
548 prev_norm = vec_norm(&r);
549 }
550
551 if prev_norm < tol * norm0 {
552 break;
553 }
554 }
555
556 Ok(x)
557 }
558
559 pub fn compare_with_jacobi(
563 &self,
564 b: &[f64],
565 n_iterations: usize,
566 ) -> SparseResult<SmootherMetrics> {
567 if b.len() != self.n {
568 return Err(SparseError::DimensionMismatch {
569 expected: self.n,
570 found: b.len(),
571 });
572 }
573
574 let mut x_learned = vec![0.0; self.n];
576 let r0 = compute_residual(
577 &self.a_values,
578 &self.a_row_ptr,
579 &self.a_col_idx,
580 &x_learned,
581 b,
582 );
583 let norm0 = vec_norm(&r0);
584
585 let mut learned_history = Vec::with_capacity(n_iterations);
586 for _ in 0..n_iterations {
587 let _ = self.v_cycle(&mut x_learned, b);
588 let r = compute_residual(
589 &self.a_values,
590 &self.a_row_ptr,
591 &self.a_col_idx,
592 &x_learned,
593 b,
594 );
595 learned_history.push(vec_norm(&r));
596 }
597
598 let mut x_jacobi = vec![0.0; self.n];
600 let mut jacobi_final_norm = norm0;
601 for _ in 0..n_iterations {
602 self.v_cycle_jacobi(&mut x_jacobi, b)?;
603 let r = compute_residual(
604 &self.a_values,
605 &self.a_row_ptr,
606 &self.a_col_idx,
607 &x_jacobi,
608 b,
609 );
610 jacobi_final_norm = vec_norm(&r);
611 }
612
613 let learned_final = learned_history.last().copied().unwrap_or(norm0);
614
615 let convergence_factor = if n_iterations > 0 && norm0 > f64::EPSILON {
617 (learned_final / norm0).powf(1.0 / n_iterations as f64)
618 } else {
619 1.0
620 };
621
622 let residual_reduction = if norm0 > f64::EPSILON {
623 learned_final / norm0
624 } else {
625 0.0
626 };
627
628 let spectral_radius_reduction = if jacobi_final_norm > f64::EPSILON {
629 learned_final / jacobi_final_norm
630 } else {
631 1.0
632 };
633
634 Ok(SmootherMetrics {
635 spectral_radius_reduction,
636 convergence_factor,
637 residual_reduction,
638 training_loss_history: learned_history,
639 })
640 }
641
642 pub fn dim(&self) -> usize {
644 self.n
645 }
646
647 pub fn coarse_dim(&self) -> usize {
649 self.n_c
650 }
651}
652
653#[cfg(test)]
658mod tests {
659 use super::*;
660 use crate::learned_smoother::linear_smoother::LinearSmoother;
661
662 fn poisson_1d(n: usize) -> (Vec<f64>, Vec<usize>, Vec<usize>) {
664 let mut values = Vec::new();
665 let mut col_idx = Vec::new();
666 let mut row_ptr = vec![0usize];
667
668 for i in 0..n {
669 if i > 0 {
670 values.push(-1.0);
671 col_idx.push(i - 1);
672 }
673 values.push(2.0);
674 col_idx.push(i);
675 if i + 1 < n {
676 values.push(-1.0);
677 col_idx.push(i + 1);
678 }
679 row_ptr.push(values.len());
680 }
681 (values, row_ptr, col_idx)
682 }
683
684 #[test]
685 fn test_coarsening_dimensions() {
686 let (_, _, _, _, _, _, n_c) = build_coarsening(7);
687 assert_eq!(n_c, 4);
688 let (_, _, _, _, _, _, n_c2) = build_coarsening(8);
689 assert_eq!(n_c2, 4);
690 }
691
692 #[test]
693 fn test_hybrid_solver_solves_poisson() {
694 let n = 7;
695 let (vals, rp, ci) = poisson_1d(n);
696
697 let smoother = LinearSmoother::from_csr(&vals, &rp, &ci, 2.0 / 3.0);
698 let config = LearnedSmootherConfig {
699 max_training_steps: 200,
700 convergence_tol: 1e-8,
701 ..LearnedSmootherConfig::default()
702 };
703
704 let solver = HybridMultigridSolver::new(
705 vals.clone(),
706 rp.clone(),
707 ci.clone(),
708 Box::new(smoother),
709 config,
710 )
711 .expect("solver creation");
712
713 let mut b = vec![0.0; n];
715 b[0] = 1.0;
716 b[n - 1] = 1.0;
717
718 let x = solver.solve(&b).expect("solve");
719
720 let r = compute_residual(&vals, &rp, &ci, &x, &b);
722 let rel_res = vec_norm(&r) / vec_norm(&b);
723 assert!(
724 rel_res < 1e-4,
725 "Relative residual should be small, got {rel_res}"
726 );
727 }
728
729 #[test]
730 fn test_compare_with_jacobi() {
731 let n = 7;
732 let (vals, rp, ci) = poisson_1d(n);
733
734 let smoother = LinearSmoother::from_csr(&vals, &rp, &ci, 2.0 / 3.0);
735 let config = LearnedSmootherConfig::default();
736
737 let solver = HybridMultigridSolver::new(
738 vals.clone(),
739 rp.clone(),
740 ci.clone(),
741 Box::new(smoother),
742 config,
743 )
744 .expect("solver creation");
745
746 let mut b = vec![1.0; n];
747 b[0] = 2.0;
748
749 let metrics = solver.compare_with_jacobi(&b, 10).expect("compare");
750
751 assert!(
752 metrics.convergence_factor < 1.0,
753 "Convergence factor should be < 1, got {}",
754 metrics.convergence_factor
755 );
756 assert_eq!(metrics.training_loss_history.len(), 10);
757 }
758
759 #[test]
760 fn test_solver_dimension_mismatch() {
761 let n = 5;
762 let (vals, rp, ci) = poisson_1d(n);
763 let smoother = LinearSmoother::from_csr(&vals, &rp, &ci, 2.0 / 3.0);
764 let config = LearnedSmootherConfig::default();
765
766 let solver = HybridMultigridSolver::new(vals, rp, ci, Box::new(smoother), config)
767 .expect("solver creation");
768
769 let b = vec![1.0; 3]; assert!(solver.solve(&b).is_err());
771 }
772
773 #[test]
774 fn test_solver_coarse_dim() {
775 let n = 9;
776 let (vals, rp, ci) = poisson_1d(n);
777 let smoother = LinearSmoother::from_csr(&vals, &rp, &ci, 2.0 / 3.0);
778 let config = LearnedSmootherConfig::default();
779
780 let solver = HybridMultigridSolver::new(vals, rp, ci, Box::new(smoother), config)
781 .expect("solver creation");
782
783 assert_eq!(solver.dim(), 9);
784 assert_eq!(solver.coarse_dim(), 5);
785 }
786
787 #[test]
788 fn test_direct_solve_small_system() {
789 let vals = vec![2.0, 1.0, 1.0, 3.0];
791 let rp = vec![0, 2, 4];
792 let ci = vec![0, 1, 0, 1];
793 let b = vec![5.0, 7.0];
794
795 let x = direct_solve(&vals, &rp, &ci, &b, 2).expect("direct solve");
796 assert!((x[0] - 1.6).abs() < 1e-10);
797 assert!((x[1] - 1.8).abs() < 1e-10);
798 }
799}