1#[cfg(any(feature = "native", feature = "wasm"))]
33use crate::parallel::{parallel_enumerate_map, parallel_map_indexed};
34use crate::sparse::CsrMatrix;
35use crate::traits::{ComplexField, Preconditioner};
36use ndarray::Array1;
37use num_traits::{FromPrimitive, Zero};
38
39#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
41pub enum AmgCoarsening {
42 #[default]
45 RugeStuben,
46
47 Pmis,
50
51 Hmis,
54}
55
56#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
58pub enum AmgInterpolation {
59 #[default]
61 Standard,
62
63 Extended,
66
67 Direct,
70}
71
72#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
74pub enum AmgSmoother {
75 #[default]
77 Jacobi,
78
79 L1Jacobi,
81
82 SymmetricGaussSeidel,
85
86 Chebyshev,
88}
89
90#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
92pub enum AmgCycle {
93 #[default]
95 VCycle,
96
97 WCycle,
99
100 FCycle,
102}
103
104#[derive(Debug, Clone)]
106pub struct AmgConfig {
107 pub coarsening: AmgCoarsening,
109
110 pub interpolation: AmgInterpolation,
112
113 pub smoother: AmgSmoother,
115
116 pub cycle: AmgCycle,
118
119 pub strong_threshold: f64,
122
123 pub max_levels: usize,
125
126 pub coarse_size: usize,
128
129 pub num_pre_smooth: usize,
131
132 pub num_post_smooth: usize,
134
135 pub jacobi_weight: f64,
137
138 pub trunc_factor: f64,
140
141 pub max_interp_elements: usize,
143
144 pub aggressive_coarsening_levels: usize,
146}
147
148impl Default for AmgConfig {
149 fn default() -> Self {
150 Self {
151 coarsening: AmgCoarsening::default(),
152 interpolation: AmgInterpolation::default(),
153 smoother: AmgSmoother::default(),
154 cycle: AmgCycle::default(),
155 strong_threshold: 0.25,
156 max_levels: 25,
157 coarse_size: 50,
158 num_pre_smooth: 1,
159 num_post_smooth: 1,
160 jacobi_weight: 0.6667, trunc_factor: 0.0,
162 max_interp_elements: 4,
163 aggressive_coarsening_levels: 0,
164 }
165 }
166}
167
168impl AmgConfig {
169 pub fn for_bem() -> Self {
174 Self {
175 strong_threshold: 0.5, coarsening: AmgCoarsening::Pmis, smoother: AmgSmoother::L1Jacobi, max_interp_elements: 6,
179 ..Default::default()
180 }
181 }
182
183 pub fn for_fem() -> Self {
185 Self {
186 strong_threshold: 0.25,
187 coarsening: AmgCoarsening::RugeStuben,
188 smoother: AmgSmoother::SymmetricGaussSeidel,
189 ..Default::default()
190 }
191 }
192
193 pub fn for_parallel() -> Self {
195 Self {
196 coarsening: AmgCoarsening::Pmis,
197 smoother: AmgSmoother::Jacobi,
198 jacobi_weight: 0.8,
199 num_pre_smooth: 2,
200 num_post_smooth: 2,
201 ..Default::default()
202 }
203 }
204
205 pub fn for_difficult_problems() -> Self {
207 Self {
208 coarsening: AmgCoarsening::RugeStuben,
209 interpolation: AmgInterpolation::Extended,
210 smoother: AmgSmoother::SymmetricGaussSeidel,
211 strong_threshold: 0.25,
212 max_interp_elements: 8,
213 num_pre_smooth: 2,
214 num_post_smooth: 2,
215 ..Default::default()
216 }
217 }
218}
219
220#[derive(Debug, Clone, Copy, PartialEq, Eq)]
222enum PointType {
223 Undecided,
225 Coarse,
227 Fine,
229}
230
231#[derive(Debug, Clone)]
233struct AmgLevel<T: ComplexField> {
234 matrix: CsrMatrix<T>,
236
237 prolongation: Option<CsrMatrix<T>>,
239
240 restriction: Option<CsrMatrix<T>>,
242
243 diag_inv: Array1<T>,
245
246 coarse_to_fine: Vec<usize>,
248
249 num_dofs: usize,
251}
252
253#[derive(Debug, Clone)]
258pub struct AmgPreconditioner<T: ComplexField> {
259 levels: Vec<AmgLevel<T>>,
261
262 config: AmgConfig,
264
265 setup_time_ms: f64,
267 grid_complexity: f64,
268 operator_complexity: f64,
269}
270
271impl<T: ComplexField> AmgPreconditioner<T>
272where
273 T::Real: Sync + Send,
274{
275 pub fn from_csr(matrix: &CsrMatrix<T>, config: AmgConfig) -> Self {
277 let start = std::time::Instant::now();
278
279 let mut levels = Vec::new();
280 let mut current_matrix = matrix.clone();
281
282 let diag_inv = Self::compute_diag_inv(¤t_matrix);
284
285 levels.push(AmgLevel {
286 matrix: current_matrix.clone(),
287 prolongation: None,
288 restriction: None,
289 diag_inv,
290 coarse_to_fine: Vec::new(),
291 num_dofs: current_matrix.num_rows,
292 });
293
294 for _level_idx in 0..config.max_levels - 1 {
296 let n = current_matrix.num_rows;
297 if n <= config.coarse_size {
298 break;
299 }
300
301 let strong_connections =
303 Self::compute_strength_matrix(¤t_matrix, config.strong_threshold);
304
305 let (point_types, coarse_to_fine) = match config.coarsening {
307 AmgCoarsening::RugeStuben => {
308 Self::coarsen_ruge_stuben(¤t_matrix, &strong_connections)
309 }
310 AmgCoarsening::Pmis | AmgCoarsening::Hmis => {
311 Self::coarsen_pmis(¤t_matrix, &strong_connections)
312 }
313 };
314
315 let num_coarse = coarse_to_fine.len();
316 if num_coarse == 0 || num_coarse >= n {
317 break;
319 }
320
321 let prolongation = Self::build_interpolation(
323 ¤t_matrix,
324 &strong_connections,
325 &point_types,
326 &coarse_to_fine,
327 &config,
328 );
329
330 let restriction = Self::transpose_csr(&prolongation);
332
333 let coarse_matrix =
335 Self::galerkin_product(&restriction, ¤t_matrix, &prolongation);
336
337 let coarse_diag_inv = Self::compute_diag_inv(&coarse_matrix);
339
340 if let Some(last) = levels.last_mut() {
342 last.prolongation = Some(prolongation);
343 last.restriction = Some(restriction);
344 last.coarse_to_fine = coarse_to_fine;
345 }
346
347 levels.push(AmgLevel {
349 matrix: coarse_matrix.clone(),
350 prolongation: None,
351 restriction: None,
352 diag_inv: coarse_diag_inv,
353 coarse_to_fine: Vec::new(),
354 num_dofs: num_coarse,
355 });
356
357 current_matrix = coarse_matrix;
358 }
359
360 let setup_time_ms = start.elapsed().as_secs_f64() * 1000.0;
361
362 let (grid_complexity, operator_complexity) = Self::compute_complexities(&levels);
364
365 Self {
366 levels,
367 config,
368 setup_time_ms,
369 grid_complexity,
370 operator_complexity,
371 }
372 }
373
374 pub fn num_levels(&self) -> usize {
376 self.levels.len()
377 }
378
379 pub fn setup_time_ms(&self) -> f64 {
381 self.setup_time_ms
382 }
383
384 pub fn grid_complexity(&self) -> f64 {
386 self.grid_complexity
387 }
388
389 pub fn operator_complexity(&self) -> f64 {
391 self.operator_complexity
392 }
393
394 pub fn config(&self) -> &AmgConfig {
396 &self.config
397 }
398
399 fn compute_diag_inv(matrix: &CsrMatrix<T>) -> Array1<T> {
401 let n = matrix.num_rows;
402 let mut diag_inv = Array1::from_elem(n, T::one());
403
404 for i in 0..n {
405 let diag = matrix.get(i, i);
406 let tol = T::Real::from_f64(1e-15).unwrap();
407 if diag.norm() > tol {
408 diag_inv[i] = diag.inv();
409 }
410 }
411
412 diag_inv
413 }
414
415 fn compute_strength_matrix(matrix: &CsrMatrix<T>, theta: f64) -> Vec<Vec<usize>> {
419 let n = matrix.num_rows;
420
421 #[cfg(any(feature = "native", feature = "wasm"))]
422 {
423 parallel_map_indexed(n, |i| {
424 let mut max_off_diag = T::Real::from_f64(0.0).unwrap();
426 for (j, val) in matrix.row_entries(i) {
427 if i != j {
428 let norm = val.norm();
429 if norm > max_off_diag {
430 max_off_diag = norm;
431 }
432 }
433 }
434
435 let threshold = T::Real::from_f64(theta).unwrap() * max_off_diag;
436
437 let mut row_strong = Vec::new();
439 for (j, val) in matrix.row_entries(i) {
440 if i != j && val.norm() >= threshold {
441 row_strong.push(j);
442 }
443 }
444 row_strong
445 })
446 }
447
448 #[cfg(not(any(feature = "native", feature = "wasm")))]
449 {
450 let mut strong: Vec<Vec<usize>> = vec![Vec::new(); n];
451 for (i, row_strong) in strong.iter_mut().enumerate().take(n) {
452 let mut max_off_diag = T::Real::from_f64(0.0).unwrap();
454 for (j, val) in matrix.row_entries(i) {
455 if i != j {
456 let norm = val.norm();
457 if norm > max_off_diag {
458 max_off_diag = norm;
459 }
460 }
461 }
462
463 let threshold = T::Real::from_f64(theta).unwrap() * max_off_diag;
464
465 for (j, val) in matrix.row_entries(i) {
467 if i != j && val.norm() >= threshold {
468 row_strong.push(j);
469 }
470 }
471 }
472 strong
473 }
474 }
475
476 fn coarsen_ruge_stuben(
478 matrix: &CsrMatrix<T>,
479 strong: &[Vec<usize>],
480 ) -> (Vec<PointType>, Vec<usize>) {
481 let n = matrix.num_rows;
482 let mut point_types = vec![PointType::Undecided; n];
483
484 let mut lambda: Vec<usize> = vec![0; n];
486 for row in strong.iter().take(n) {
487 for &j in row {
488 lambda[j] += 1;
489 }
490 }
491
492 let mut order: Vec<usize> = (0..n).collect();
494 order.sort_by(|&a, &b| lambda[b].cmp(&lambda[a]));
495
496 for &i in &order {
498 if point_types[i] != PointType::Undecided {
499 continue;
500 }
501
502 point_types[i] = PointType::Coarse;
504
505 for j in 0..n {
507 if point_types[j] == PointType::Undecided && strong[j].contains(&i) {
508 point_types[j] = PointType::Fine;
509 for &k in &strong[j] {
511 if point_types[k] == PointType::Undecided {
512 lambda[k] = lambda[k].saturating_sub(1);
513 }
514 }
515 }
516 }
517 }
518
519 for pt in &mut point_types {
521 if *pt == PointType::Undecided {
522 *pt = PointType::Fine;
523 }
524 }
525
526 let coarse_to_fine: Vec<usize> = (0..n)
528 .filter(|&i| point_types[i] == PointType::Coarse)
529 .collect();
530
531 (point_types, coarse_to_fine)
532 }
533
534 fn coarsen_pmis(matrix: &CsrMatrix<T>, strong: &[Vec<usize>]) -> (Vec<PointType>, Vec<usize>) {
536 let n = matrix.num_rows;
537 let mut point_types = vec![PointType::Undecided; n];
538
539 let weights: Vec<f64> = (0..n)
541 .map(|i| {
542 strong[i].len() as f64 + (i as f64 * 0.0001) % 0.001
544 })
545 .collect();
546
547 let mut changed = true;
549 let mut iteration = 0;
550 const MAX_ITERATIONS: usize = 100;
551
552 while changed && iteration < MAX_ITERATIONS {
553 changed = false;
554 iteration += 1;
555
556 #[cfg(any(feature = "native", feature = "wasm"))]
557 {
558 let updates: Vec<(usize, PointType)> =
560 parallel_enumerate_map(&point_types, |i, pt| {
561 if *pt != PointType::Undecided {
562 return (i, *pt);
563 }
564
565 let mut is_max = true;
567 for &j in &strong[i] {
568 if point_types[j] == PointType::Undecided && weights[j] > weights[i] {
569 is_max = false;
570 break;
571 }
572 }
573
574 let has_c_neighbor = strong[i]
576 .iter()
577 .any(|&j| point_types[j] == PointType::Coarse);
578
579 if has_c_neighbor {
580 (i, PointType::Fine)
581 } else if is_max {
582 (i, PointType::Coarse)
583 } else {
584 (i, PointType::Undecided)
585 }
586 });
587
588 for (i, new_type) in updates {
589 if point_types[i] != new_type {
590 point_types[i] = new_type;
591 changed = true;
592 }
593 }
594 }
595
596 #[cfg(not(any(feature = "native", feature = "wasm")))]
597 {
598 let old_types = point_types.clone();
600 for i in 0..n {
601 if old_types[i] != PointType::Undecided {
602 continue;
603 }
604
605 let mut is_max = true;
607 for &j in &strong[i] {
608 if old_types[j] == PointType::Undecided && weights[j] > weights[i] {
609 is_max = false;
610 break;
611 }
612 }
613
614 let has_c_neighbor =
616 strong[i].iter().any(|&j| old_types[j] == PointType::Coarse);
617
618 if has_c_neighbor {
619 point_types[i] = PointType::Fine;
620 changed = true;
621 } else if is_max {
622 point_types[i] = PointType::Coarse;
623 changed = true;
624 }
625 }
626 }
627 }
628
629 for pt in &mut point_types {
631 if *pt == PointType::Undecided {
632 *pt = PointType::Coarse;
633 }
634 }
635
636 let coarse_to_fine: Vec<usize> = (0..n)
638 .filter(|&i| point_types[i] == PointType::Coarse)
639 .collect();
640
641 (point_types, coarse_to_fine)
642 }
643
644 fn build_interpolation(
646 matrix: &CsrMatrix<T>,
647 strong: &[Vec<usize>],
648 point_types: &[PointType],
649 coarse_to_fine: &[usize],
650 config: &AmgConfig,
651 ) -> CsrMatrix<T> {
652 let n_fine = matrix.num_rows;
653 let n_coarse = coarse_to_fine.len();
654
655 let mut fine_to_coarse = vec![usize::MAX; n_fine];
657 for (coarse_idx, &fine_idx) in coarse_to_fine.iter().enumerate() {
658 fine_to_coarse[fine_idx] = coarse_idx;
659 }
660
661 let mut triplets: Vec<(usize, usize, T)> = Vec::new();
663
664 for i in 0..n_fine {
665 match point_types[i] {
666 PointType::Coarse => {
667 let coarse_idx = fine_to_coarse[i];
669 triplets.push((i, coarse_idx, T::one()));
670 }
671 PointType::Fine => {
672 let a_ii = matrix.get(i, i);
674
675 let c_neighbors: Vec<usize> = strong[i]
677 .iter()
678 .copied()
679 .filter(|&j| point_types[j] == PointType::Coarse)
680 .collect();
681
682 if c_neighbors.is_empty() {
683 let mut best_c = usize::MAX;
687 let mut max_val = T::Real::zero();
688
689 for (j, val) in matrix.row_entries(i) {
690 if point_types[j] == PointType::Coarse {
691 let norm = val.norm();
692 if norm > max_val {
693 max_val = norm;
694 best_c = j;
695 }
696 }
697 }
698
699 if best_c != usize::MAX {
700 triplets.push((i, fine_to_coarse[best_c], T::one()));
701 } else {
702 if !coarse_to_fine.is_empty() {
706 triplets.push((i, 0, T::one()));
707 }
708 }
709 continue;
710 }
711
712 match config.interpolation {
714 AmgInterpolation::Direct | AmgInterpolation::Standard => {
715 let mut weights: Vec<(usize, T)> = Vec::new();
716 let mut sum_weights = T::zero();
717
718 for &j in &c_neighbors {
719 let a_ij = matrix.get(i, j);
720 let tol = T::Real::from_f64(1e-15).unwrap();
721 if a_ii.norm() > tol {
722 let w = T::zero() - a_ij * a_ii.inv();
723 weights.push((fine_to_coarse[j], w));
724 sum_weights += w;
725 }
726 }
727
728 if config.interpolation == AmgInterpolation::Standard {
730 let mut weak_sum = T::zero();
731 for (j, val) in matrix.row_entries(i) {
732 if j != i && !c_neighbors.contains(&j) {
733 weak_sum += val;
734 }
735 }
736
737 let tol = T::Real::from_f64(1e-15).unwrap();
738 if sum_weights.norm() > tol && weak_sum.norm() > tol {
739 let scale = T::one() + weak_sum * (a_ii * sum_weights).inv();
740 for (_, w) in &mut weights {
741 *w *= scale;
742 }
743 }
744 }
745
746 if config.trunc_factor > 0.0 {
748 let max_w = weights.iter().map(|(_, w)| w.norm()).fold(
749 T::Real::from_f64(0.0).unwrap(),
750 |a, b| {
751 if a > b { a } else { b }
752 },
753 );
754 let threshold =
755 T::Real::from_f64(config.trunc_factor).unwrap() * max_w;
756 weights.retain(|(_, w)| w.norm() >= threshold);
757
758 if weights.len() > config.max_interp_elements {
759 weights.sort_by(|a, b| {
760 b.1.norm().partial_cmp(&a.1.norm()).unwrap()
761 });
762 weights.truncate(config.max_interp_elements);
763 }
764 }
765
766 for (coarse_idx, w) in weights {
767 triplets.push((i, coarse_idx, w));
768 }
769 }
770 AmgInterpolation::Extended => {
771 let mut weights: Vec<(usize, T)> = Vec::new();
772
773 for &j in &c_neighbors {
775 let a_ij = matrix.get(i, j);
776 let tol = T::Real::from_f64(1e-15).unwrap();
777 if a_ii.norm() > tol {
778 let w = T::zero() - a_ij * a_ii.inv();
779 weights.push((fine_to_coarse[j], w));
780 }
781 }
782
783 let f_neighbors: Vec<usize> = strong[i]
785 .iter()
786 .copied()
787 .filter(|&j| point_types[j] == PointType::Fine)
788 .collect();
789
790 for &k in &f_neighbors {
791 let a_ik = matrix.get(i, k);
792 let a_kk = matrix.get(k, k);
793
794 let tol = T::Real::from_f64(1e-15).unwrap();
795 if a_kk.norm() < tol {
796 continue;
797 }
798
799 for &j in &strong[k] {
800 if point_types[j] == PointType::Coarse {
801 let a_kj = matrix.get(k, j);
802 let w = T::zero() - a_ik * a_kj * (a_ii * a_kk).inv();
803
804 let coarse_j = fine_to_coarse[j];
805 if let Some((_, existing)) =
806 weights.iter_mut().find(|(idx, _)| *idx == coarse_j)
807 {
808 *existing += w;
809 } else {
810 weights.push((coarse_j, w));
811 }
812 }
813 }
814 }
815
816 if weights.len() > config.max_interp_elements {
817 weights
818 .sort_by(|a, b| b.1.norm().partial_cmp(&a.1.norm()).unwrap());
819 weights.truncate(config.max_interp_elements);
820 }
821
822 for (coarse_idx, w) in weights {
823 triplets.push((i, coarse_idx, w));
824 }
825 }
826 }
827 }
828 PointType::Undecided => {}
829 }
830 }
831
832 CsrMatrix::from_triplets(n_fine, n_coarse, triplets)
833 }
834
835 fn transpose_csr(matrix: &CsrMatrix<T>) -> CsrMatrix<T> {
837 let m = matrix.num_rows;
838 let n = matrix.num_cols;
839
840 let mut triplets: Vec<(usize, usize, T)> = Vec::new();
841 for i in 0..m {
842 for (j, val) in matrix.row_entries(i) {
843 triplets.push((j, i, val));
844 }
845 }
846
847 CsrMatrix::from_triplets(n, m, triplets)
848 }
849
850 fn galerkin_product(r: &CsrMatrix<T>, a: &CsrMatrix<T>, p: &CsrMatrix<T>) -> CsrMatrix<T> {
852 let ap = a.matmul(p);
853 r.matmul(&ap)
854 }
855
856 #[allow(dead_code)]
858 fn sparse_matmul(a: &CsrMatrix<T>, b: &CsrMatrix<T>) -> CsrMatrix<T> {
859 a.matmul(b)
860 }
861
862 fn compute_complexities(levels: &[AmgLevel<T>]) -> (f64, f64) {
864 if levels.is_empty() {
865 return (1.0, 1.0);
866 }
867
868 let fine_dofs = levels[0].num_dofs as f64;
869 let fine_nnz = levels[0].matrix.nnz() as f64;
870
871 let total_dofs: f64 = levels.iter().map(|l| l.num_dofs as f64).sum();
872 let total_nnz: f64 = levels.iter().map(|l| l.matrix.nnz() as f64).sum();
873
874 let grid_complexity = total_dofs / fine_dofs;
875 let operator_complexity = total_nnz / fine_nnz;
876
877 (grid_complexity, operator_complexity)
878 }
879
880 fn smooth_jacobi(
882 matrix: &CsrMatrix<T>,
883 diag_inv: &Array1<T>,
884 x: &mut Array1<T>,
885 b: &Array1<T>,
886 omega: f64,
887 num_sweeps: usize,
888 ) {
889 let omega = T::from_real(T::Real::from_f64(omega).unwrap());
890 let n = x.len();
891
892 for _ in 0..num_sweeps {
893 let r = b - &matrix.matvec(x);
894
895 #[cfg(any(feature = "native", feature = "wasm"))]
896 {
897 let updates: Vec<T> = parallel_map_indexed(n, |i| omega * diag_inv[i] * r[i]);
898 for (i, delta) in updates.into_iter().enumerate() {
899 x[i] += delta;
900 }
901 }
902
903 #[cfg(not(any(feature = "native", feature = "wasm")))]
904 {
905 for i in 0..n {
906 x[i] += omega * diag_inv[i] * r[i];
907 }
908 }
909 }
910 }
911
912 fn smooth_l1_jacobi(
914 matrix: &CsrMatrix<T>,
915 x: &mut Array1<T>,
916 b: &Array1<T>,
917 num_sweeps: usize,
918 ) {
919 let n = x.len();
920
921 let l1_diag: Vec<T::Real> = (0..n)
922 .map(|i| {
923 let mut sum = T::Real::from_f64(0.0).unwrap();
924 for (_, val) in matrix.row_entries(i) {
925 sum += val.norm();
926 }
927 let tol = T::Real::from_f64(1e-15).unwrap();
928 if sum > tol {
929 sum
930 } else {
931 T::Real::from_f64(1.0).unwrap()
932 }
933 })
934 .collect();
935
936 for _ in 0..num_sweeps {
937 let r = b - &matrix.matvec(x);
938
939 #[cfg(any(feature = "native", feature = "wasm"))]
940 {
941 let updates: Vec<T> =
942 parallel_map_indexed(n, |i| r[i] * T::from_real(l1_diag[i]).inv());
943 for (i, delta) in updates.into_iter().enumerate() {
944 x[i] += delta;
945 }
946 }
947
948 #[cfg(not(any(feature = "native", feature = "wasm")))]
949 {
950 for i in 0..n {
951 x[i] += r[i] * T::from_real(l1_diag[i]).inv();
952 }
953 }
954 }
955 }
956
957 fn smooth_sym_gauss_seidel(
959 matrix: &CsrMatrix<T>,
960 x: &mut Array1<T>,
961 b: &Array1<T>,
962 num_sweeps: usize,
963 ) {
964 let n = x.len();
965 let tol = T::Real::from_f64(1e-15).unwrap();
966
967 for _ in 0..num_sweeps {
968 for i in 0..n {
970 let mut sum = b[i];
971 let mut diag = T::one();
972
973 for (j, val) in matrix.row_entries(i) {
974 if j == i {
975 diag = val;
976 } else {
977 sum -= val * x[j];
978 }
979 }
980
981 if diag.norm() > tol {
982 x[i] = sum * diag.inv();
983 }
984 }
985
986 for i in (0..n).rev() {
988 let mut sum = b[i];
989 let mut diag = T::one();
990
991 for (j, val) in matrix.row_entries(i) {
992 if j == i {
993 diag = val;
994 } else {
995 sum -= val * x[j];
996 }
997 }
998
999 if diag.norm() > tol {
1000 x[i] = sum * diag.inv();
1001 }
1002 }
1003 }
1004 }
1005
1006 fn v_cycle(&self, level: usize, x: &mut Array1<T>, b: &Array1<T>) {
1008 let lvl = &self.levels[level];
1009
1010 if level == self.levels.len() - 1 || lvl.prolongation.is_none() {
1012 match self.config.smoother {
1013 AmgSmoother::Jacobi | AmgSmoother::Chebyshev => {
1014 Self::smooth_jacobi(
1015 &lvl.matrix,
1016 &lvl.diag_inv,
1017 x,
1018 b,
1019 self.config.jacobi_weight,
1020 20,
1021 );
1022 }
1023 AmgSmoother::L1Jacobi => {
1024 Self::smooth_l1_jacobi(&lvl.matrix, x, b, 20);
1025 }
1026 AmgSmoother::SymmetricGaussSeidel => {
1027 Self::smooth_sym_gauss_seidel(&lvl.matrix, x, b, 10);
1028 }
1029 }
1030 return;
1031 }
1032
1033 match self.config.smoother {
1035 AmgSmoother::Jacobi | AmgSmoother::Chebyshev => {
1036 Self::smooth_jacobi(
1037 &lvl.matrix,
1038 &lvl.diag_inv,
1039 x,
1040 b,
1041 self.config.jacobi_weight,
1042 self.config.num_pre_smooth,
1043 );
1044 }
1045 AmgSmoother::L1Jacobi => {
1046 Self::smooth_l1_jacobi(&lvl.matrix, x, b, self.config.num_pre_smooth);
1047 }
1048 AmgSmoother::SymmetricGaussSeidel => {
1049 Self::smooth_sym_gauss_seidel(&lvl.matrix, x, b, self.config.num_pre_smooth);
1050 }
1051 }
1052
1053 let r = b - &lvl.matrix.matvec(x);
1055
1056 let r_coarse = lvl.restriction.as_ref().unwrap().matvec(&r);
1058
1059 let n_coarse = self.levels[level + 1].num_dofs;
1061 let mut e_coarse = Array1::from_elem(n_coarse, T::zero());
1062
1063 self.v_cycle(level + 1, &mut e_coarse, &r_coarse);
1065
1066 let e = lvl.prolongation.as_ref().unwrap().matvec(&e_coarse);
1068
1069 *x = x.clone() + e;
1071
1072 match self.config.smoother {
1074 AmgSmoother::Jacobi | AmgSmoother::Chebyshev => {
1075 Self::smooth_jacobi(
1076 &lvl.matrix,
1077 &lvl.diag_inv,
1078 x,
1079 b,
1080 self.config.jacobi_weight,
1081 self.config.num_post_smooth,
1082 );
1083 }
1084 AmgSmoother::L1Jacobi => {
1085 Self::smooth_l1_jacobi(&lvl.matrix, x, b, self.config.num_post_smooth);
1086 }
1087 AmgSmoother::SymmetricGaussSeidel => {
1088 Self::smooth_sym_gauss_seidel(&lvl.matrix, x, b, self.config.num_post_smooth);
1089 }
1090 }
1091 }
1092}
1093
1094impl<T: ComplexField> Preconditioner<T> for AmgPreconditioner<T>
1095where
1096 T::Real: Sync + Send,
1097{
1098 fn apply(&self, r: &Array1<T>) -> Array1<T> {
1099 if self.levels.is_empty() {
1100 return r.clone();
1101 }
1102
1103 let n = self.levels[0].num_dofs;
1104 if r.len() != n {
1105 return r.clone();
1106 }
1107
1108 let mut z = Array1::from_elem(n, T::zero());
1109
1110 match self.config.cycle {
1111 AmgCycle::VCycle => {
1112 self.v_cycle(0, &mut z, r);
1113 }
1114 AmgCycle::WCycle => {
1115 self.v_cycle(0, &mut z, r);
1116 self.v_cycle(0, &mut z, r);
1117 }
1118 AmgCycle::FCycle => {
1119 self.v_cycle(0, &mut z, r);
1120 let residual = r - &self.levels[0].matrix.matvec(&z);
1121 let mut correction = Array1::from_elem(n, T::zero());
1122 self.v_cycle(0, &mut correction, &residual);
1123 z = z + correction;
1124 }
1125 }
1126
1127 z
1128 }
1129}
1130
1131#[derive(Debug, Clone)]
1133pub struct AmgDiagnostics {
1134 pub num_levels: usize,
1136 pub grid_complexity: f64,
1138 pub operator_complexity: f64,
1140 pub setup_time_ms: f64,
1142 pub level_dofs: Vec<usize>,
1144 pub level_nnz: Vec<usize>,
1146}
1147
1148impl<T: ComplexField> AmgPreconditioner<T> {
1149 pub fn diagnostics(&self) -> AmgDiagnostics {
1151 AmgDiagnostics {
1152 num_levels: self.levels.len(),
1153 grid_complexity: self.grid_complexity,
1154 operator_complexity: self.operator_complexity,
1155 setup_time_ms: self.setup_time_ms,
1156 level_dofs: self.levels.iter().map(|l| l.num_dofs).collect(),
1157 level_nnz: self.levels.iter().map(|l| l.matrix.nnz()).collect(),
1158 }
1159 }
1160}
1161
1162#[cfg(test)]
1163mod tests {
1164 use super::*;
1165 use num_complex::Complex64;
1166
1167 fn create_1d_laplacian(n: usize) -> CsrMatrix<Complex64> {
1169 let mut triplets: Vec<(usize, usize, Complex64)> = Vec::new();
1170
1171 for i in 0..n {
1172 triplets.push((i, i, Complex64::new(2.0, 0.0)));
1173 if i > 0 {
1174 triplets.push((i, i - 1, Complex64::new(-1.0, 0.0)));
1175 }
1176 if i < n - 1 {
1177 triplets.push((i, i + 1, Complex64::new(-1.0, 0.0)));
1178 }
1179 }
1180
1181 CsrMatrix::from_triplets(n, n, triplets)
1182 }
1183
1184 #[test]
1185 fn test_amg_creation() {
1186 let matrix = create_1d_laplacian(100);
1187 let config = AmgConfig::default();
1188
1189 let amg = AmgPreconditioner::from_csr(&matrix, config);
1190
1191 assert!(amg.num_levels() >= 2);
1192 assert!(amg.grid_complexity() >= 1.0);
1193 assert!(amg.operator_complexity() >= 1.0);
1194 }
1195
1196 #[test]
1197 fn test_amg_apply() {
1198 let matrix = create_1d_laplacian(50);
1199 let config = AmgConfig::default();
1200 let amg = AmgPreconditioner::from_csr(&matrix, config);
1201
1202 let r = Array1::from_vec((0..50).map(|i| Complex64::new(i as f64, 0.0)).collect());
1203
1204 let z = amg.apply(&r);
1205
1206 assert_eq!(z.len(), r.len());
1207
1208 let diff: f64 = (&z - &r).iter().map(|x| x.norm()).sum();
1209 assert!(diff > 1e-10, "Preconditioner should modify the vector");
1210 }
1211
1212 #[test]
1213 fn test_amg_pmis_coarsening() {
1214 let matrix = create_1d_laplacian(100);
1215 let config = AmgConfig {
1216 coarsening: AmgCoarsening::Pmis,
1217 ..Default::default()
1218 };
1219
1220 let amg = AmgPreconditioner::from_csr(&matrix, config);
1221 assert!(amg.num_levels() >= 2);
1222 }
1223
1224 #[test]
1225 fn test_amg_different_smoothers() {
1226 let matrix = create_1d_laplacian(50);
1227 let r = Array1::from_vec((0..50).map(|i| Complex64::new(i as f64, 0.0)).collect());
1228
1229 for smoother in [
1230 AmgSmoother::Jacobi,
1231 AmgSmoother::L1Jacobi,
1232 AmgSmoother::SymmetricGaussSeidel,
1233 ] {
1234 let config = AmgConfig {
1235 smoother,
1236 ..Default::default()
1237 };
1238 let amg = AmgPreconditioner::from_csr(&matrix, config);
1239
1240 let z = amg.apply(&r);
1241 assert_eq!(z.len(), r.len());
1242 }
1243 }
1244
1245 #[test]
1246 fn test_amg_reduces_residual() {
1247 let n = 64;
1248 let matrix = create_1d_laplacian(n);
1249 let config = AmgConfig::default();
1250 let amg = AmgPreconditioner::from_csr(&matrix, config);
1251
1252 let b = Array1::from_vec(
1253 (0..n)
1254 .map(|i| Complex64::new((i as f64).sin(), 0.0))
1255 .collect(),
1256 );
1257
1258 let mut x = Array1::from_elem(n, Complex64::new(0.0, 0.0));
1259
1260 let r0 = &b - &matrix.matvec(&x);
1261 let norm_r0: f64 = r0.iter().map(|v| v.norm_sqr()).sum::<f64>().sqrt();
1262
1263 for _ in 0..10 {
1264 let r = &b - &matrix.matvec(&x);
1265 let z = amg.apply(&r);
1266 x = x + z;
1267 }
1268
1269 let rf = &b - &matrix.matvec(&x);
1270 let norm_rf: f64 = rf.iter().map(|v| v.norm_sqr()).sum::<f64>().sqrt();
1271
1272 assert!(
1273 norm_rf < norm_r0 * 0.1,
1274 "AMG should significantly reduce residual: {} -> {}",
1275 norm_r0,
1276 norm_rf
1277 );
1278 }
1279
1280 #[test]
1281 fn test_diagnostics() {
1282 let matrix = create_1d_laplacian(100);
1283 let amg = AmgPreconditioner::from_csr(&matrix, AmgConfig::default());
1284
1285 let diag = amg.diagnostics();
1286
1287 assert!(diag.num_levels >= 2);
1288 assert_eq!(diag.level_dofs.len(), diag.num_levels);
1289 assert_eq!(diag.level_nnz.len(), diag.num_levels);
1290 assert!(diag.grid_complexity >= 1.0);
1291 assert!(diag.setup_time_ms >= 0.0);
1292 }
1293}